Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FAH uses ids using the same naming policy as rolloutPolicy #6743

Merged
merged 9 commits into from
Feb 6, 2024
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 { 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 @@
deleteTime: string;
}

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

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

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

export interface Traffic {
name: string;
// oneof traffic_management
Expand Down Expand Up @@ -248,7 +261,7 @@
done: boolean;
// oneof result
error?: Status;
response?: any;

Check warning on line 264 in src/gcp/apphosting.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
// end oneof result
}

Expand Down Expand Up @@ -334,6 +347,33 @@
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 @@
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 All @@ -410,7 +463,7 @@
// correct.
const trafficCopy = { ...traffic };
if ("rolloutPolicy" in traffic) {
trafficCopy.rolloutPolicy = {} as any;

Check warning on line 466 in src/gcp/apphosting.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

Check warning on line 466 in src/gcp/apphosting.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
}
const fieldMasks = proto.fieldMasks(trafficCopy);
const queryParams = {
Expand Down Expand Up @@ -441,10 +494,13 @@
* 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 @@ -456,7 +512,56 @@
/**
* Ensure that Frameworks API is enabled on the project.
*/
export async function ensureApiEnabled(options: any): Promise<void> {

Check warning on line 515 in src/gcp/apphosting.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
const projectId = needProjectId(options);

Check warning on line 516 in src/gcp/apphosting.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `{ projectId?: string | undefined; project?: string | undefined; rc?: RC | undefined; }`
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.

Check warning on line 522 in src/gcp/apphosting.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected @param names to be "projectId, location, backendId, counter". Got "counter"
*/
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, but exports.listRollouts gives you type errors or something? That's unfortunate :(

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 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 All @@ -29,7 +29,7 @@
/**
* Set up a new App Hosting backend.
*/
export async function doSetup(setup: any, projectId: string): Promise<void> {

Check warning on line 32 in src/init/features/apphosting/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
await Promise.all([
ensure(projectId, "cloudbuild.googleapis.com", "apphosting", true),
ensure(projectId, "secretmanager.googleapis.com", "apphosting", true),
Expand All @@ -39,8 +39,8 @@

const allowedLocations = (await apphosting.listLocations(projectId)).map((loc) => loc.locationId);

if (setup.location) {

Check warning on line 42 in src/init/features/apphosting/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .location on an `any` value
if (!allowedLocations.includes(setup.location)) {

Check warning on line 43 in src/init/features/apphosting/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `string`

Check warning on line 43 in src/init/features/apphosting/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .location on an `any` value
throw new FirebaseError(
`Invalid location ${setup.location}. Valid choices are ${allowedLocations.join(", ")}`,
);
Expand Down Expand Up @@ -209,17 +209,16 @@
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