diff --git a/src/commands/index.ts b/src/commands/index.ts index b3fa211894e..1b3c5d3bc30 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -150,8 +150,6 @@ export function load(client: any): any { client.internaltesting.frameworks.compose = loadCommand("internaltesting-frameworks-compose"); client.internaltesting.functions = {}; client.internaltesting.functions.discover = loadCommand("internaltesting-functions-discover"); - client.internaltesting.frameworks = {}; - client.internaltesting.frameworks.init = loadCommand("internaltesting-frameworks-init"); } client.login = loadCommand("login"); client.login.add = loadCommand("login-add"); diff --git a/src/commands/internaltesting-frameworks-init.ts b/src/commands/internaltesting-frameworks-init.ts deleted file mode 100644 index 729fde0e9d9..00000000000 --- a/src/commands/internaltesting-frameworks-init.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Command } from "../command"; -import { linkGitHubRepository } from "../init/features/composer/repo"; -import { Options } from "../options"; -import { needProjectId } from "../projectUtils"; -import requireInteractive from "../requireInteractive"; - -export const command = new Command("internaltesting:frameworks:init") - .description("connect github repo to cloud build") - .before(requireInteractive) - .action(async (options: Options) => { - const projectId = needProjectId(options); - await linkGitHubRepository(projectId, "us-central2", "stack0"); - // TODO: send repo metadata to control plane - }); diff --git a/src/api/frameworks.ts b/src/gcp/frameworks.ts similarity index 84% rename from src/api/frameworks.ts rename to src/gcp/frameworks.ts index 30fcc41bd66..b74b8dbff20 100644 --- a/src/api/frameworks.ts +++ b/src/gcp/frameworks.ts @@ -1,7 +1,7 @@ import { Client } from "../apiv2"; import { frameworksOrigin } from "../api"; -export const API_VERSION = "v1"; +export const API_VERSION = "v2"; const client = new Client({ urlPrefix: frameworksOrigin, @@ -9,7 +9,7 @@ const client = new Client({ apiVersion: API_VERSION, }); -export type State = "BUILDING" | "BUILD" | "DEPLOYING" | "READY" | "FAILED"; +type State = "BUILDING" | "BUILD" | "DEPLOYING" | "READY" | "FAILED"; interface Codebase { repository?: string; @@ -17,7 +17,7 @@ interface Codebase { } /** A Stack, the primary resource of Frameworks. */ -interface Stack { +export interface Stack { name: string; mode?: string; codebase: Codebase; @@ -29,7 +29,7 @@ interface Stack { export type StackOutputOnlyFields = "createTime" | "updateTime" | "uri"; -interface Build { +export interface Build { name: string; state: State; error: Status; @@ -61,7 +61,7 @@ interface CodebaseSource { // end oneof reference } -export interface OperationMetadata { +interface OperationMetadata { createTime: string; endTime: string; target: string; @@ -87,14 +87,15 @@ export interface Operation { export async function createStack( projectId: string, location: string, - stackId: string, - stack: Stack + stackInput: Omit ): Promise { + const stackId = stackInput.name; const res = await client.post, Operation>( `projects/${projectId}/locations/${location}/stacks`, - stack, + stackInput, { queryParams: { stackId } } ); + return res.body; } @@ -105,13 +106,14 @@ export async function createBuild( projectId: string, location: string, stackId: string, - buildId: string, - build: Build + buildInput: Omit ): Promise { + const buildId = buildInput.name; const res = await client.post, Operation>( `projects/${projectId}/locations/${location}/stacks/${stackId}/builds`, - build, + buildInput, { queryParams: { buildId } } ); + return res.body; } diff --git a/src/init/features/frameworks/index.ts b/src/init/features/frameworks/index.ts index 8b75e5023c9..e26b126b2dd 100644 --- a/src/init/features/frameworks/index.ts +++ b/src/init/features/frameworks/index.ts @@ -8,11 +8,26 @@ import { DEFAULT_DEPLOY_METHOD, ALLOWED_DEPLOY_METHODS, } from "./constants"; +import { linkGitHubRepository } from "./repo"; +import { Stack, StackOutputOnlyFields } from "../../../gcp/frameworks"; +import { Repository } from "../../../gcp/cloudbuild"; +import * as poller from "../../../operation-poller"; +import { frameworksOrigin } from "../../../api"; +import * as gcp from "../../../gcp/frameworks"; +import { API_VERSION } from "../../../gcp/frameworks"; + +const frameworksPollerOptions: Omit = { + apiOrigin: frameworksOrigin, + apiVersion: API_VERSION, + masterTimeout: 25 * 60 * 1_000, + maxBackoff: 10_000, +}; /** * Setup new frameworks project. */ export async function doSetup(setup: any): Promise { + const projectId: string = setup?.rcfile?.projects?.default; setup.frameworks = {}; utils.logBullet("First we need a few details to create your service."); @@ -54,4 +69,42 @@ export async function doSetup(setup: any): Promise { }, setup.frameworks ); + + if (setup.frameworks.deployMethod === "github") { + const cloudBuildConnRepo = await linkGitHubRepository( + projectId, + setup.frameworks.region, + setup.frameworks.serviceName + ); + toStack(cloudBuildConnRepo, setup.frameworks.serviceName); + } +} + +function toStack( + cloudBuildConnRepo: Repository, + stackId: string +): Omit { + return { + name: stackId, + codebase: { repository: cloudBuildConnRepo.name, rootDirectory: "/" }, + labels: {}, + }; +} + +/** + * Creates Stack object from long running operations. + */ +export async function createStack( + projectId: string, + location: string, + stackInput: Omit +): Promise { + const op = await gcp.createStack(projectId, location, stackInput); + const stack = await poller.pollOperation({ + ...frameworksPollerOptions, + pollerName: `create-${projectId}-${location}-${stackInput.name}`, + operationResourceName: op.name, + }); + + return stack; } diff --git a/src/init/features/composer/repo.ts b/src/init/features/frameworks/repo.ts similarity index 97% rename from src/init/features/composer/repo.ts rename to src/init/features/frameworks/repo.ts index c5080ffa73f..5b726b855f5 100644 --- a/src/init/features/composer/repo.ts +++ b/src/init/features/frameworks/repo.ts @@ -25,10 +25,6 @@ function extractRepoSlugFromURI(remoteUri: string): string | undefined { return match[1]; } -function generateConnectionId(stackId: string): string { - return `composer-${stackId}-conn`; -} - /** * Generates a repository ID. * N.B. The deterministic nature of the repository ID implies that each @@ -48,7 +44,7 @@ export async function linkGitHubRepository( location: string, stackId: string ): Promise { - const connectionId = generateConnectionId(stackId); + const connectionId = stackId; await getOrCreateConnection(projectId, location, connectionId); let remoteUri = await promptRepositoryURI(projectId, location, connectionId); diff --git a/src/test/init/frameworks/index.spec.ts b/src/test/init/frameworks/index.spec.ts new file mode 100644 index 00000000000..1534f014f55 --- /dev/null +++ b/src/test/init/frameworks/index.spec.ts @@ -0,0 +1,61 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as gcp from "../../../gcp/frameworks"; +import * as poller from "../../../operation-poller"; +import { createStack } from "../../../init/features/frameworks/index"; + +describe("operationsConverter", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let pollOperationStub: sinon.SinonStub; + let createStackStub: sinon.SinonStub; + + beforeEach(() => { + pollOperationStub = sandbox + .stub(poller, "pollOperation") + .throws("Unexpected pollOperation call"); + createStackStub = sandbox.stub(gcp, "createStack").throws("Unexpected createStack call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + describe("createStack", () => { + const projectId = "projectId"; + const location = "us-central1"; + const stackId = "stackId"; + const stackInput = { + name: stackId, + codebase: { + repository: `projects/${projectId}/locations/${location}/connections/${stackId}`, + rootDirectory: "/", + }, + labels: {}, + }; + const op = { + name: `projects/${projectId}/locations/${location}/stacks/${stackId}`, + done: true, + }; + const completeStack = { + name: `projects/${projectId}/locations/${location}/stacks/${stackId}`, + codebase: { + repository: `projects/${projectId}/locations/${location}/connections/${stackId}`, + rootDirectory: "/", + }, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + it("checks is correct arguments are sent & creates a stack", async () => { + createStackStub.resolves(op); + pollOperationStub.resolves(completeStack); + + await createStack(projectId, location, stackInput); + expect(createStackStub).to.be.calledWith(projectId, location, stackInput); + }); + }); +}); diff --git a/src/test/init/features/composer.spec.ts b/src/test/init/frameworks/repo.spec.ts similarity index 98% rename from src/test/init/features/composer.spec.ts rename to src/test/init/frameworks/repo.spec.ts index 661bdd831aa..1c21b608b43 100644 --- a/src/test/init/features/composer.spec.ts +++ b/src/test/init/frameworks/repo.spec.ts @@ -5,7 +5,7 @@ import * as gcb from "../../../gcp/cloudbuild"; import * as prompt from "../../../prompt"; import * as poller from "../../../operation-poller"; import { FirebaseError } from "../../../error"; -import * as repo from "../../../init/features/composer/repo"; +import * as repo from "../../../init/features/frameworks/repo"; import * as utils from "../../../utils"; describe("composer", () => {