diff --git a/src/commands/frameworks-stacks-create.ts b/src/commands/frameworks-stacks-create.ts new file mode 100644 index 00000000000..5ffe85a9c12 --- /dev/null +++ b/src/commands/frameworks-stacks-create.ts @@ -0,0 +1,13 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import requireInteractive from "../requireInteractive"; +import { doSetup } from "../init/features/frameworks"; + +export const command = new Command("stacks:create") + .description("Create a stack in a Firebase project") + .before(requireInteractive) + .action(async (options: Options) => { + const projectId = needProjectId(options); + await doSetup(options, projectId); + }); diff --git a/src/commands/frameworks-stacks-delete.ts b/src/commands/frameworks-stacks-delete.ts new file mode 100644 index 00000000000..8916e856b10 --- /dev/null +++ b/src/commands/frameworks-stacks-delete.ts @@ -0,0 +1,43 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { FirebaseError } from "../error"; +import * as gcp from "../gcp/frameworks"; +import { promptOnce } from "../prompt"; +import * as utils from "../utils"; + +export const command = new Command("stacks:delete") + .description("Delete a stack from a Firebase project") + .option("-l, --location ", "App Backend location", "us-central1") + .option("-s, --stackId ", "Stack Id", "") + .withForce() + .action(async (options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const stackId = options.stackId as string; + if (!stackId) { + throw new FirebaseError("Stack id can't be empty."); + } + const confirmDeletion = await promptOnce( + { + type: "confirm", + name: "force", + default: false, + message: "You are about to delete the Stack with id: " + stackId + "\n Are you sure?", + }, + options + ); + if (!confirmDeletion) { + throw new FirebaseError("Deletion aborted."); + } + + try { + await gcp.deleteStack(projectId, location, stackId); + utils.logSuccess(`Successfully deleted the stack: ${stackId}`); + } catch (err: any) { + throw new FirebaseError( + `Failed to delete stack: ${stackId}. Please check the parameters you have provided.`, + { original: err } + ); + } + }); diff --git a/src/commands/frameworks-stacks-get.ts b/src/commands/frameworks-stacks-get.ts new file mode 100644 index 00000000000..91059921654 --- /dev/null +++ b/src/commands/frameworks-stacks-get.ts @@ -0,0 +1,35 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import * as gcp from "../gcp/frameworks"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; + +export const command = new Command("stacks:get") + .description("Get stack details of a Firebase project") + .option("-l, --location ", "App Backend location", "us-central1") + .option("--s, --stackId ", "Stack Id", "") + .action(async (options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const stackId = options.stackId as string; + if (!stackId) { + throw new FirebaseError("Stack id can't be empty."); + } + + let stack; + try { + stack = await gcp.getStack(projectId, location, stackId); + /** + * TODO print this in a prettier way. + */ + logger.info(stack); + } catch (err: any) { + throw new FirebaseError( + `Failed to get stack: ${stackId}. Please check the parameters you have provided.`, + { original: err } + ); + } + + return stack; + }); diff --git a/src/commands/frameworks-stacks-list.ts b/src/commands/frameworks-stacks-list.ts new file mode 100644 index 00000000000..61ccd6bbb16 --- /dev/null +++ b/src/commands/frameworks-stacks-list.ts @@ -0,0 +1,30 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import * as gcp from "../gcp/frameworks"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; + +export const command = new Command("stacks:list") + .description("List stacks of a Firebase project.") + .option("-l, --location ", "App Backend location", "us-central1") + .action(async (options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + + let stacks; + try { + stacks = await gcp.listStack(projectId, location); + /** + * TODO print this in a prettier way. + */ + logger.info(stacks); + } catch (err: any) { + throw new FirebaseError( + `Unable to list stacks present in project: ${projectId}. Please check the parameters you have provided.`, + { original: err } + ); + } + + return stacks; + }); diff --git a/src/commands/index.ts b/src/commands/index.ts index 1b3c5d3bc30..4814d4f66fe 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -151,6 +151,14 @@ export function load(client: any): any { client.internaltesting.functions = {}; client.internaltesting.functions.discover = loadCommand("internaltesting-functions-discover"); } + if (experiments.isEnabled("internalframeworks")) { + client.frameworks = {}; + client.frameworks.stacks = {}; + client.frameworks.stacks.list = loadCommand("frameworks-stacks-list"); + client.frameworks.stacks.create = loadCommand("frameworks-stacks-create"); + client.frameworks.stacks.create = loadCommand("frameworks-stacks-get"); + client.frameworks.stacks.create = loadCommand("frameworks-stacks-delete"); + } client.login = loadCommand("login"); client.login.add = loadCommand("login-add"); client.login.ci = loadCommand("login-ci"); diff --git a/src/gcp/frameworks.ts b/src/gcp/frameworks.ts index 9cf26631805..cc7f3a5f4d2 100644 --- a/src/gcp/frameworks.ts +++ b/src/gcp/frameworks.ts @@ -27,7 +27,7 @@ export interface Stack { uri: string; } -export type StackOutputOnlyFields = "createTime" | "updateTime" | "uri" | "codebase"; +export type StackOutputOnlyFields = "name" | "createTime" | "updateTime" | "uri"; export interface Build { name: string; @@ -81,19 +81,23 @@ export interface Operation { // end oneof result } +export interface ListStacksResponse { + stacks: Stack[]; +} + /** * Creates a new Stack in a given project and location. */ export async function createStack( projectId: string, location: string, - stackInput: Omit + stackReqBoby: Omit, + backendId: string ): Promise { - const stackId = stackInput.name; const res = await client.post, Operation>( - `projects/${projectId}/locations/${location}/stacks`, - stackInput, - { queryParams: { stackId } } + `projects/${projectId}/locations/${location}/backends`, + stackReqBoby, + { queryParams: { backendId } } ); return res.body; @@ -107,12 +111,36 @@ export async function getStack( location: string, stackId: string ): Promise { - const name = `projects/${projectId}/locations/${location}/stacks/${stackId}`; + const name = `projects/${projectId}/locations/${location}/backends/${stackId}`; const res = await client.get(name); return res.body; } +/** + * List all stacks present in a project and region. + */ +export async function listStack(projectId: string, location: string): Promise { + const name = `projects/${projectId}/locations/${location}/backends`; + const res = await client.get(name); + + return res.body; +} + +/** + * Deletes a Stack with stackId in a given project and location. + */ +export async function deleteStack( + projectId: string, + location: string, + stackId: string +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${stackId}`; + const res = await client.delete(name); + + return res.body; +} + /** * Creates a new Build in a given project and location. */ @@ -124,7 +152,7 @@ export async function createBuild( ): Promise { const buildId = buildInput.name; const res = await client.post, Operation>( - `projects/${projectId}/locations/${location}/stacks/${stackId}/builds`, + `projects/${projectId}/locations/${location}/backends/${stackId}/builds`, buildInput, { queryParams: { buildId } } ); diff --git a/src/init/features/frameworks/index.ts b/src/init/features/frameworks/index.ts index 26c0ebc160b..52be59193d6 100644 --- a/src/init/features/frameworks/index.ts +++ b/src/init/features/frameworks/index.ts @@ -27,8 +27,7 @@ const frameworksPollerOptions: Omit { - const projectId: string = setup?.rcfile?.projects?.default; +export async function doSetup(setup: any, projectId: string): Promise { setup.frameworks = {}; utils.logBullet("First we need a few details to create your service."); @@ -71,15 +70,18 @@ export async function doSetup(setup: any): Promise { setup.frameworks ); - await getOrCreateStack(projectId, setup); + const stack: Stack | undefined = await getOrCreateStack(projectId, setup); + if (stack) { + utils.logSuccess(`Successfully created a stack: ${stack.name}`); + } } -function toStack( - cloudBuildConnRepo: Repository, - stackId: string -): Omit { +function toStack(cloudBuildConnRepo: Repository): Omit { return { - name: stackId, + codebase: { + repository: `${cloudBuildConnRepo.name}`, + rootDirectory: "/", + }, labels: {}, }; } @@ -96,13 +98,9 @@ export async function getOrCreateStack(projectId: string, setup: any): Promise + stackReqBoby: Omit, + stackId: string ): Promise { - const op = await gcp.createStack(projectId, location, stackInput); + const op = await gcp.createStack(projectId, location, stackReqBoby, stackId); const stack = await poller.pollOperation({ ...frameworksPollerOptions, - pollerName: `create-${projectId}-${location}-${stackInput.name}`, + pollerName: `create-${projectId}-${location}-${stackId}`, operationResourceName: op.name, }); diff --git a/src/init/features/frameworks/repo.ts b/src/init/features/frameworks/repo.ts index 0586c7339b4..1cbe8169f7a 100644 --- a/src/init/features/frameworks/repo.ts +++ b/src/init/features/frameworks/repo.ts @@ -27,13 +27,21 @@ function extractRepoSlugFromURI(remoteUri: string): string | undefined { /** * Generates a repository ID. - * N.B. The deterministic nature of the repository ID implies that each - * Cloud Build Connection will have one Cloud Build Repo child resource. - * The current implementation is subject to change in the event that - * the 1:1 Connection-to-Resource relationship no longer holds. + * The relation is 1:* between Cloud Build Connection and Github Repositories. */ -function generateRepositoryId(): string | undefined { - return `composer-repo`; +function generateRepositoryId(remoteUri: string): string | undefined { + return extractRepoSlugFromURI(remoteUri)?.replaceAll("/", "-"); +} + +/** + * The 'frameworks-' is prefixed, to seperate the Cloud Build connections created from + * Frameworks platforms with rest of manually created Cloud Build connections. + * + * The reason suffix 'location' is because of + * 1:1 relation between location and Cloud Build connection. + */ +function generateConnectionId(location: string): string { + return `frameworks-${location}`; } /** @@ -41,10 +49,9 @@ function generateRepositoryId(): string | undefined { */ export async function linkGitHubRepository( projectId: string, - location: string, - stackId: string + location: string ): Promise { - const connectionId = stackId; + const connectionId = generateConnectionId(location); await getOrCreateConnection(projectId, location, connectionId); let remoteUri = await promptRepositoryURI(projectId, location, connectionId); @@ -147,7 +154,7 @@ export async function getOrCreateRepository( connectionId: string, remoteUri: string ): Promise { - const repositoryId = generateRepositoryId(); + const repositoryId = generateRepositoryId(remoteUri); if (!repositoryId) { throw new FirebaseError(`Failed to generate repositoryId for URI "${remoteUri}".`); } diff --git a/src/test/init/frameworks/index.spec.ts b/src/test/init/frameworks/index.spec.ts index ccc7763a940..28ea17f0fd3 100644 --- a/src/test/init/frameworks/index.spec.ts +++ b/src/test/init/frameworks/index.spec.ts @@ -34,10 +34,6 @@ describe("operationsConverter", () => { const projectId = "projectId"; const location = "us-central1"; const stackId = "stackId"; - const stackInput = { - name: stackId, - labels: {}, - }; const op = { name: `projects/${projectId}/locations/${location}/stacks/${stackId}`, done: true, @@ -58,17 +54,23 @@ describe("operationsConverter", () => { }, }; const cloudBuildConnRepo = { - name: `projects/${projectId}/locations/${location}/stacks/${stackId}`, + name: `projects/${projectId}/locations/${location}/connections/framework-${location}/repositories/repoId`, remoteUri: "remoteUri", createTime: "0", updateTime: "1", }; - + const stackInput = { + codebase: { + repository: cloudBuildConnRepo.name, + rootDirectory: "/", + }, + labels: {}, + }; it("should createStack", async () => { createStackStub.resolves(op); pollOperationStub.resolves(completeStack); - await createStack(projectId, location, stackInput); + await createStack(projectId, location, stackInput, stackId); expect(createStackStub).to.be.calledWith(projectId, location, stackInput); }); @@ -86,10 +88,8 @@ describe("operationsConverter", () => { const newStackId = "newStackId"; const newPath = `projects/${projectId}/locations/${location}/stacks/${newStackId}`; setup.frameworks.serviceName = newStackId; - stackInput.name = newStackId; op.name = newPath; completeStack.name = newPath; - cloudBuildConnRepo.name = newPath; getStackStub.throws(new FirebaseError("error", { status: 404 })); linkGitHubRepositoryStub.resolves(cloudBuildConnRepo); createStackStub.resolves(op); diff --git a/src/test/init/frameworks/repo.spec.ts b/src/test/init/frameworks/repo.spec.ts index 1c21b608b43..9b80636762f 100644 --- a/src/test/init/frameworks/repo.spec.ts +++ b/src/test/init/frameworks/repo.spec.ts @@ -46,8 +46,7 @@ describe("composer", () => { describe("connect GitHub repo", () => { const projectId = "projectId"; const location = "us-central1"; - const stackId = "stack0"; - const connectionId = `composer-${stackId}-conn`; + const connectionId = `frameworks-${location}`; const op = { name: `projects/${projectId}/locations/${location}/connections/${connectionId}`, @@ -119,7 +118,7 @@ describe("composer", () => { projectId, location, connectionId, - "composer-repo", + "test-repo0", repos.repositories[0].remoteUri ); }); @@ -128,7 +127,7 @@ describe("composer", () => { getConnectionStub.resolves(pendingConn); fetchLinkableRepositoriesStub.resolves({ repositories: [] }); - await expect(repo.linkGitHubRepository(projectId, location, stackId)).to.be.rejected; + await expect(repo.linkGitHubRepository(projectId, location)).to.be.rejected; }); }); });