From c5cad6bfcaa5be0f6693723fc6b4d48885e746e0 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 13 Dec 2023 15:32:22 -0800 Subject: [PATCH] Add utility commands for app hosting. --- src/commands/apphosting-builds-create.ts | 32 ++++++++++++++++++++++ src/commands/apphosting-builds-get.ts | 17 ++++++++++++ src/commands/apphosting-rollouts-create.ts | 25 +++++++++++++++++ src/commands/apphosting-rollouts-list.ts | 21 ++++++++++++++ src/commands/index.ts | 6 ++++ src/gcp/apphosting.ts | 32 ++++++++++++++++++++-- src/init/features/apphosting/index.ts | 16 +---------- src/utils.ts | 17 ++++++++++++ 8 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 src/commands/apphosting-builds-create.ts create mode 100644 src/commands/apphosting-builds-get.ts create mode 100644 src/commands/apphosting-rollouts-create.ts create mode 100644 src/commands/apphosting-rollouts-list.ts diff --git a/src/commands/apphosting-builds-create.ts b/src/commands/apphosting-builds-create.ts new file mode 100644 index 00000000000..b028bbe68aa --- /dev/null +++ b/src/commands/apphosting-builds-create.ts @@ -0,0 +1,32 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { generateId } from "../utils"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:builds:create ") + .description("Create a build for an App Hosting backend") + .option("-l, --location ", "Specify the region of the backend", "us-central1") + .option("-i, --id ", "Id of the build. If not present, autogenerate a random id", "") + .option("-b, --branch ", "Repository branch to deploy. Defaults to 'main'", "main") + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const buildId = (options.buildId as string) || generateId(); + const branch = options.branch as string; + + const op = await apphosting.createBuild(projectId, location, backendId, buildId, { + source: { + codebase: { + branch: "main", + }, + }, + }); + + logger.info(`Started a build for backend ${backendId} on branch ${branch}.`); + logger.info("Check status by running:"); + logger.info(`\tfirebase apphosting:builds:get ${backendId} ${buildId} --location ${location}`); + return op; + }); diff --git a/src/commands/apphosting-builds-get.ts b/src/commands/apphosting-builds-get.ts new file mode 100644 index 00000000000..5a080c690ad --- /dev/null +++ b/src/commands/apphosting-builds-get.ts @@ -0,0 +1,17 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:builds:get ") + .description("Create a build for an App Hosting backend") + .option("-l, --location ", "Specify the region of the backend", "us-central1") + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, buildId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const build = await apphosting.getBuild(projectId, location, backendId, buildId); + logger.info(JSON.stringify(build, null, 2)); + return build; + }); diff --git a/src/commands/apphosting-rollouts-create.ts b/src/commands/apphosting-rollouts-create.ts new file mode 100644 index 00000000000..3ed549788ec --- /dev/null +++ b/src/commands/apphosting-rollouts-create.ts @@ -0,0 +1,25 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { generateId } from "../utils"; + +export const command = new Command("apphosting:rollouts:create ") + .description("Create a build for an App Hosting backend") + .option("-l, --location ", "Specify the region of the backend", "us-central1") + .option("-i, --id ", "Id of the rollout. If not present, autogenerate a random id", "") + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, buildId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const rolloutId = (options.buildId as string) || generateId(); + const build = `projects/${projectId}/backends/${backendId}/builds/${buildId}`; + const op = await apphosting.createRollout(projectId, location, backendId, rolloutId, { + build, + }); + logger.info(`Started a rollout for backend ${backendId} with build ${buildId}.`); + logger.info("Check status by running:"); + logger.info(`\tfirebase apphosting:rollouts:list --location ${location}`); + return op; + }); diff --git a/src/commands/apphosting-rollouts-list.ts b/src/commands/apphosting-rollouts-list.ts new file mode 100644 index 00000000000..e3a404bf851 --- /dev/null +++ b/src/commands/apphosting-rollouts-list.ts @@ -0,0 +1,21 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; + +export const command = new Command("apphosting:rollouts:list ") + .description("List rollouts of an App Hosting backend") + .option( + "-l, --location ", + "Rgion of the rollouts. Defaults to listing rollouts from all regions", + "-" + ) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + const location = options.location as string; + const rollouts = await apphosting.listRollouts(projectId, location, backendId); + logger.info(JSON.stringify(rollouts, null, 2)); + return rollouts; + }); diff --git a/src/commands/index.ts b/src/commands/index.ts index 387ba489507..5f0a129e7ab 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -157,6 +157,12 @@ export function load(client: any): any { client.apphosting.backends.create = loadCommand("apphosting-backends-create"); client.apphosting.backends.get = loadCommand("apphosting-backends-get"); client.apphosting.backends.delete = loadCommand("apphosting-backends-delete"); + client.apphosting.builds = {}; + client.apphosting.builds.get = loadCommand("apphosting-builds-get"); + client.apphosting.builds.create = loadCommand("apphosting-builds-create"); + client.apphosting.rollouts = {}; + client.apphosting.rollouts.create = loadCommand("apphosting-rollouts-create"); + client.apphosting.rollouts.list = loadCommand("apphosting-rollouts-list"); } client.login = loadCommand("login"); client.login.add = loadCommand("login-add"); diff --git a/src/gcp/apphosting.ts b/src/gcp/apphosting.ts index 75cf71f8c0f..c79bf17554f 100644 --- a/src/gcp/apphosting.ts +++ b/src/gcp/apphosting.ts @@ -303,6 +303,20 @@ export async function deleteBackend( return res.body; } +/** + * Get a Build by Id + */ +export async function getBuild( + projectId: string, + location: string, + backendId: string, + buildId: string +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`; + const res = await client.get(name); + return res.body; +} + /** * Creates a new Build in a given project and location. */ @@ -322,7 +336,7 @@ export async function createBuild( } /** - * Create a new rollout for a backend + * Create a new rollout for a backend. */ export async function createRollout( projectId: string, @@ -340,7 +354,21 @@ export async function createRollout( } /** - * Update traffic of a backend + * List all rollouts for a backend. + */ +export async function listRollouts( + projectId: string, + location: string, + backendId: string +): Promise { + const res = await client.get<{ rollouts: Rollout[] }>( + `projects/${projectId}/locations/${location}/backends/${backendId}/rollouts` + ); + return res.body.rollouts; +} + +/** + * Update traffic of a backend. */ export async function updateTraffic( projectId: string, diff --git a/src/init/features/apphosting/index.ts b/src/init/features/apphosting/index.ts index bacc497e0f7..85c2b01baf9 100644 --- a/src/init/features/apphosting/index.ts +++ b/src/init/features/apphosting/index.ts @@ -2,7 +2,7 @@ import * as clc from "colorette"; import * as repo from "./repo"; import * as poller from "../../../operation-poller"; import * as apphosting from "../../../gcp/apphosting"; -import { logBullet, logSuccess, logWarning } from "../../../utils"; +import { generateId, logBullet, logSuccess, logWarning } from "../../../utils"; import { apphostingOrigin } from "../../../api"; import { Backend, @@ -205,17 +205,3 @@ export async function onboardRollout( logSuccess("Rollout completed."); return { rollout, build }; } - -/** - * Only lowercase, digits, and hyphens; must begin with letter, and cannot end with hyphen - */ -function generateId(n = 6): string { - const letters = "abcdefghijklmnopqrstuvwxyz"; - const allChars = "01234567890-abcdefghijklmnopqrstuvwxyz"; - let id = letters[Math.floor(Math.random() * letters.length)]; - for (let i = 1; i < n; i++) { - const idx = Math.floor(Math.random() * allChars.length); - id += allChars[idx]; - } - return id; -} diff --git a/src/utils.ts b/src/utils.ts index 93ea9e005f0..ab14ba678a0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -810,3 +810,20 @@ export function getHostnameFromUrl(url: string): string | null { return null; } } + +/** + * Generate id meeting the following criterias: + * - Lowercase, digits, and hyphens only + * - Must begin with letter + * - Cannot end with hyphen + */ +export function generateId(n = 6): string { + const letters = "abcdefghijklmnopqrstuvwxyz"; + const allChars = "01234567890-abcdefghijklmnopqrstuvwxyz"; + let id = letters[Math.floor(Math.random() * letters.length)]; + for (let i = 1; i < n; i++) { + const idx = Math.floor(Math.random() * allChars.length); + id += allChars[idx]; + } + return id; +}