Skip to content

Commit

Permalink
FAH uses ids using the same naming policy as rolloutPolicy (#6743)
Browse files Browse the repository at this point in the history
* FAH uses ids using the same naming policy as rolloutPolicy

* lint fixes

* s/getNextBuildId/getNextRolloutId

* Pad build incrementer per offline chat

* Fix pagination and padding

* Remove unused function. Fix style issues

* PR feedback

* I can't get 'satisfies' to help me avoid stupidity
  • Loading branch information
inlined committed Feb 6, 2024
1 parent 1473c04 commit 8e388c1
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 39 deletions.
5 changes: 3 additions & 2 deletions src/commands/apphosting-builds-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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 <backendId>")
Expand All @@ -14,7 +13,9 @@ export const command = new Command("apphosting:builds:create <backendId>")
.action(async (backendId: string, options: Options) => {
const projectId = needProjectId(options);
const location = options.location as string;
const buildId = (options.buildId as string) || generateId();
const buildId =
(options.buildId as string) ||
(await apphosting.getNextRolloutId(projectId, location, backendId));
const branch = options.branch as string;

const op = await apphosting.createBuild(projectId, location, backendId, buildId, {
Expand Down
6 changes: 4 additions & 2 deletions src/commands/apphosting-rollouts-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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 <backendId> <buildId>")
.description("Create a build for an App Hosting backend")
Expand All @@ -13,7 +12,10 @@ export const command = new Command("apphosting:rollouts:create <backendId> <buil
.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();
// TODO: Should we just reuse the buildId?
const rolloutId =
(options.buildId as string) ||
(await apphosting.getNextRolloutId(projectId, location, backendId));
const build = `projects/${projectId}/backends/${backendId}/builds/${buildId}`;
const op = await apphosting.createRollout(projectId, location, backendId, rolloutId, {
build,
Expand Down
7 changes: 6 additions & 1 deletion src/commands/apphosting-rollouts-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const command = new Command("apphosting:rollouts:list <backendId>")
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));
if (rollouts.unreachable) {
logger.error(
`WARNING: the following locations were unreachable: ${rollouts.unreachable.join(", ")}`,
);
}
logger.info(JSON.stringify(rollouts.rollouts, null, 2));
return rollouts;
});
121 changes: 113 additions & 8 deletions src/gcp/apphosting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { needProjectId } from "../projectUtils";
import { apphostingOrigin } from "../api";
import { ensure } from "../ensureApiEnabled";
import * as deploymentTool from "../deploymentTool";
import { FirebaseError } from "../error";

export const API_HOST = new URL(apphostingOrigin).host;
export const API_VERSION = "v1alpha";

const client = new Client({
export const client = new Client({
urlPrefix: apphostingOrigin,
auth: true,
apiVersion: API_VERSION,
Expand Down Expand Up @@ -63,6 +64,12 @@ export interface Build {
deleteTime: string;
}

export interface ListBuildsResponse {
builds: Build[];
nextPageToken?: string;
unreachable?: string[];
}

export type BuildOutputOnlyFields =
| "state"
| "error"
Expand Down Expand Up @@ -158,6 +165,12 @@ export type RolloutOutputOnlyFields =
| "etag"
| "reconciling";

export interface ListRolloutsResponse {
rollouts: Rollout[];
unreachable: string[];
nextPageToken?: string;
}

export interface Traffic {
name: string;
// oneof traffic_management
Expand Down Expand Up @@ -334,6 +347,33 @@ export async function getBuild(
return res.body;
}

/**
* List Builds by backend
*/
export async function listBuilds(
projectId: string,
location: string,
backendId: string,
): Promise<ListBuildsResponse> {
const name = `projects/${projectId}/locations/${location}/backends/${backendId}/builds`;
let pageToken: string | undefined;
const res: ListBuildsResponse = {
builds: [],
unreachable: [],
};

do {
const queryParams: Record<string, string> = pageToken ? { pageToken } : {};
const int = await client.get<ListBuildsResponse>(name, { queryParams });
res.builds.push(...(int.body.builds || []));
res.unreachable?.push(...(int.body.unreachable || []));
pageToken = int.body.nextPageToken;
} while (pageToken);

res.unreachable = [...new Set(res.unreachable)];
return res;
}

/**
* Creates a new Build in a given project and location.
*/
Expand Down Expand Up @@ -389,11 +429,24 @@ export async function listRollouts(
projectId: string,
location: string,
backendId: string,
): Promise<Rollout[]> {
const res = await client.get<{ rollouts: Rollout[] }>(
`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`,
);
return res.body.rollouts;
): Promise<ListRolloutsResponse> {
const name = `projects/${projectId}/locations/${location}/backends/${backendId}/rollouts`;
let pageToken: string | undefined = undefined;
const res: ListRolloutsResponse = {
rollouts: [],
unreachable: [],
};

do {
const queryParams: Record<string, string> = pageToken ? { pageToken } : {};
const int = await client.get<ListRolloutsResponse>(name, { queryParams });
res.rollouts.splice(res.rollouts.length, 0, ...(int.body.rollouts || []));
res.unreachable.splice(res.unreachable.length, 0, ...(int.body.unreachable || []));
pageToken = int.body.nextPageToken;
} while (pageToken);

res.unreachable = [...new Set(res.unreachable)];
return res;
}

/**
Expand Down Expand Up @@ -441,10 +494,13 @@ interface ListLocationsResponse {
* Lists information about the supported locations.
*/
export async function listLocations(projectId: string): Promise<Location[]> {
let pageToken;
let pageToken: string | undefined = undefined;
let locations: Location[] = [];
do {
const response = await client.get<ListLocationsResponse>(`projects/${projectId}/locations`);
const queryParams: Record<string, string> = pageToken ? { pageToken } : {};
const response = await client.get<ListLocationsResponse>(`projects/${projectId}/locations`, {
queryParams,
});
if (response.body.locations && response.body.locations.length > 0) {
locations = locations.concat(response.body.locations);
}
Expand All @@ -460,3 +516,52 @@ export async function ensureApiEnabled(options: any): Promise<void> {
const projectId = needProjectId(options);
return await ensure(projectId, API_HOST, "frameworks", true);
}

/**
* Generates the next build ID to fit with the naming scheme of the backend API.
* @param counter Overrides the counter to use, avoiding an API call.
*/
export async function getNextRolloutId(
projectId: string,
location: string,
backendId: string,
counter?: number,
): Promise<string> {
const date = new Date();
const year = date.getUTCFullYear();
// Note: month is 0 based in JS
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
const day = String(date.getUTCDay()).padStart(2, "0");

if (counter) {
return `build-${year}-${month}-${day}-${String(counter).padStart(3, "0")}`;
}

// Note: must use exports here so that listRollouts can be stubbed in tests.
const builds = await (exports as { listRollouts: typeof listRollouts }).listRollouts(
projectId,
location,
backendId,
);
if (builds.unreachable?.includes(location)) {
throw new FirebaseError(
`Firebase App Hosting is currently unreachable in location ${location}`,
);
}

let highest = 0;
const test = new RegExp(
`projects/${projectId}/locations/${location}/backends/${backendId}/rollouts/build-${year}-${month}-${day}-(\\d+)`,
);
for (const rollout of builds.rollouts) {
const match = rollout.name.match(test);
if (!match) {
continue;
}
const n = Number(match[1]);
if (n > highest) {
highest = n;
}
}
return `build-${year}-${month}-${day}-${String(highest + 1).padStart(3, "0")}`;
}
9 changes: 4 additions & 5 deletions src/init/features/apphosting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { generateId, logBullet, logSuccess, logWarning } from "../../../utils";
import { logBullet, logSuccess, logWarning } from "../../../utils";
import { apphostingOrigin } from "../../../api";
import {
Backend,
Expand Down Expand Up @@ -209,17 +209,16 @@ export async function onboardRollout(
buildInput: Omit<BuildInput, "name">,
): Promise<{ rollout: Rollout; build: Build }> {
logBullet("Starting a new rollout... this may take a few minutes.");
const buildId = generateId();
const buildId = await apphosting.getNextRolloutId(projectId, location, backendId, 1);
const buildOp = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);

const rolloutId = generateId();
const rolloutOp = await apphosting.createRollout(projectId, location, backendId, rolloutId, {
const rolloutOp = await apphosting.createRollout(projectId, location, backendId, buildId, {
build: `projects/${projectId}/locations/${location}/backends/${backendId}/builds/${buildId}`,
});

const rolloutPoll = poller.pollOperation<Rollout>({
...apphostingPollerOptions,
pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${rolloutId}`,
pollerName: `create-${projectId}-${location}-backend-${backendId}-rollout-${buildId}`,
operationResourceName: rolloutOp.name,
});
const buildPoll = poller.pollOperation<Build>({
Expand Down

0 comments on commit 8e388c1

Please sign in to comment.