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

Improve FAH create & delete #6726

Merged
merged 7 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 35 additions & 13 deletions src/commands/apphosting-backends-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import { promptOnce } from "../prompt";
import { logger } from "../logger";
import { DEFAULT_REGION } from "../init/features/apphosting/constants";
import { last } from "../utils";
import * as utils from "../utils";
import * as apphosting from "../gcp/apphosting";

const Table = require("cli-table");

Check warning on line 12 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

Check warning on line 12 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Require statement not part of import statement

const COLUMN_LENGTH = 20;
const TABLE_HEAD = [
Expand All @@ -29,16 +30,13 @@
.action(async (options: Options) => {
const projectId = needProjectId(options);
let location = options.location as string;
const backendId = options.backend as string;
if (!backendId) {
throw new FirebaseError("Backend id can't be empty.");
}
let backendId = options.backend as string;

if (!location) {
const allowedLocations = (await apphosting.listLocations(projectId)).map(
(loc) => loc.locationId,
);
location = await promptOnce({

Check warning on line 39 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
name: "region",
type: "list",
default: DEFAULT_REGION,
Expand All @@ -47,24 +45,30 @@
});
}

let backend: apphosting.Backend;
if (backendId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, this is odd. According to the approved proposal, the backend ID is supposed to be a required parameter, not a flag. That eliminates the need to pick a backend at all.

Copy link
Member Author

Choose a reason for hiding this comment

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

Irrespective of the proposal, do you like this better? It seemed odd to me that location could be browsed but backend couldn't. If you like this better we can push it through API council. As an EAP API, do you feel like it is OK to release and then get approval, or should I splice this into another CL until after approval?

Copy link
Contributor

Choose a reason for hiding this comment

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

I personally don't like it better, because I hate the prompt selector flow in a CLI 🤷‍♂️ It's not a wrong thing to do, but I'm assuming that there were product discussions that brought us to the API proposal (as that's a requirement at the top of the document) and that the pattern in the proposal was agreed upon by all the relevant stakeholders already. If you'd like to make a change to what was proposed there, starting back in the process, involving the right people, and getting an addendum would be appropriate.

That said, I thought the general pattern was to:

  1. require backendID as a parameter in the command
  2. default to looking at us-central1 as we 'traditionally' have

If someone is outside of us-central1, they get to include the --location flag.

Having this flow also makes it more complex. Sticking with the picker, you have to also consider what happens in non-interactive environment (like a CI system). As written, this would hang, I believe, which may be unexpected (instead of simply failing out).

Copy link
Member Author

Choose a reason for hiding this comment

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

🫡 I'll move the changes into another branch and bring up the idea in the devx room

Copy link
Member Author

Choose a reason for hiding this comment

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

(If it's chosen that we should keep the existing dialog, I agree that we need to check for nonInteractive)

try {
backend = await apphosting.getBackend(projectId, location, backendId);
} catch (err: any) {

Check warning on line 52 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
throw new FirebaseError(`No backends found with given parameters. Command aborted.`, {
original: err,

Check warning on line 54 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
});
}
} else {
backend = await pickBackend(projectId, location);
backendId = last(backend.name.split("/"));
}

const table = new Table({

Check warning on line 62 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

Check warning on line 62 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe construction of an any type value
head: TABLE_HEAD,
style: { head: ["green"] },
});
table.colWidths = COLUMN_LENGTH;

Check warning on line 66 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .colWidths on an `any` value

let backend;
try {
backend = await apphosting.getBackend(projectId, location, backendId);
populateTable(backend, table);
} catch (err: any) {
throw new FirebaseError(`No backends found with given parameters. Command aborted.`, {
original: err,
});
}
populateTable(backend, table);

utils.logWarning("You are about to permanently delete the backend:");
logger.info(table.toString());

Check warning on line 71 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

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

Check warning on line 71 in src/commands/apphosting-backends-delete.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

const confirmDeletion = await promptOnce(
{
Expand Down Expand Up @@ -112,3 +116,21 @@
});
table.push(newRow);
}

async function pickBackend(projectId: string, location: string): Promise<apphosting.Backend> {
const backendList = await apphosting.listBackends(projectId, location);
if (!backendList.backends.length) {
throw new FirebaseError(`No backends found in location ${location}`);
}
if (backendList.backends.length === 1) {
return backendList.backends[0];
}
const backendIds = backendList.backends.map((backend) => last(backend.name.split("/")));
const backendId = await promptOnce({
name: "backend",
type: "list",
message: "Please select the backend you'd like to delete:",
choices: backendIds,
});
return backendList.backends.find((backend) => last(backend.name.split("/")) === backendId)!;
}
31 changes: 24 additions & 7 deletions src/gcp/apphosting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type BuildOutputOnlyFields =
| "error"
| "image"
| "sourceRef"
| "buildLogUri"
| "buildLogsUri"
| "reconciling"
| "uuid"
| "etag"
Expand Down Expand Up @@ -196,10 +196,20 @@ export interface RolloutPolicy {
// end oneof trigger
stages?: RolloutStage[];
disabled?: boolean;

// TODO: This will be undefined if disabled is not true, right?
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it should be.

disabledTime: string;
}

export type RolloutPolicyOutputOnlyFields = "disabledtime";
export type RolloutPolicyOutputOnlyFields = "disabledTime";

export type RolloutPolicyInput = Omit<RolloutPolicy, RolloutPolicyOutputOnlyFields | "stages"> & {
stages: Omit<RolloutStage, "startTime" | "endTime">[];
};

export type TrafficInput = Omit<Traffic, TrafficOutputOnlyFields | "rolloutPolicy"> & {
rolloutPolicy: RolloutPolicyInput;
};

export type RolloutProgression =
| "PROGRESSION_UNSPECIFIED"
Expand Down Expand Up @@ -297,8 +307,8 @@ export async function deleteBackend(
location: string,
backendId: string,
): Promise<Operation> {
const name = `projects/${projectId}/locations/${location}/backends/${backendId}?force=true`;
const res = await client.delete<Operation>(name);
const name = `projects/${projectId}/locations/${location}/backends/${backendId}`;
const res = await client.delete<Operation>(name, { queryParams: { force: "true" } });

return res.body;
}
Expand Down Expand Up @@ -374,14 +384,21 @@ export async function updateTraffic(
projectId: string,
location: string,
backendId: string,
traffic: Omit<Traffic, TrafficOutputOnlyFields | "name">,
traffic: Omit<TrafficInput, "name">,
): Promise<Operation> {
const fieldMasks = proto.fieldMasks(traffic);
// BUG(b/322891558): setting deep fields on rolloutPolicy doesn't work for some
// reason. Create a copy without deep fields to force the updateMask to be
// correct.
const trafficCopy = { ...traffic };
if ("rolloutPolicy" in traffic) {
trafficCopy.rolloutPolicy = {} as any;
}
const fieldMasks = proto.fieldMasks(trafficCopy);
const queryParams = {
updateMask: fieldMasks.join(","),
};
const name = `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`;
const res = await client.patch<Omit<Traffic, TrafficOutputOnlyFields>, Operation>(
const res = await client.patch<TrafficInput, Operation>(
name,
{ ...traffic, name },
{
Expand Down
43 changes: 35 additions & 8 deletions src/init/features/apphosting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { FirebaseError } from "../../../error";
import { promptOnce } from "../../../prompt";
import { DEFAULT_REGION } from "./constants";
import { ensure } from "../../../ensureApiEnabled";
import * as deploymentTool from "../../../deploymentTool";

const apphostingPollerOptions: Omit<poller.OperationPollerOptions, "operationResourceName"> = {
apiOrigin: apphostingOrigin,
Expand Down Expand Up @@ -76,6 +77,33 @@ export async function doSetup(setup: any, projectId: string): Promise<void> {

const backend = await onboardBackend(projectId, location, backendId);

// TODO: Once tag patterns are implemented, prompt which method the user
// prefers. We could reduce the nubmer of questions asked by letting people
// enter tag:<pattern>?
const branch = await promptOnce({
name: "branch",
type: "input",
default: "main",
message: "Pick a branch for continuous deployment",
});
const traffic: Omit<apphosting.TrafficInput, "name"> = {
rolloutPolicy: {
codebaseBranch: branch,
stages: [
{
progression: "IMMEDIATE",
targetPercent: 100,
},
],
},
};
const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
await poller.pollOperation<apphosting.Traffic>({
...apphostingPollerOptions,
pollerName: `updateTraffic-${projectId}-${location}-${backendId}`,
operationResourceName: op.name,
});

const confirmRollout = await promptOnce({
type: "confirm",
name: "rollout",
Expand All @@ -89,13 +117,6 @@ export async function doSetup(setup: any, projectId: string): Promise<void> {
return;
}

const branch = await promptOnce({
name: "branch",
type: "input",
default: "main",
message: "Which branch do you want to deploy?",
});

const { build } = await onboardRollout(projectId, location, backendId, {
source: {
codebase: {
Expand All @@ -105,6 +126,12 @@ export async function doSetup(setup: any, projectId: string): Promise<void> {
});

if (build.state !== "READY") {
if (!build.buildLogsUri) {
throw new FirebaseError(
"Failed to build your app, but failed to get build logs as well. " +
"This is an internal error and should be reported",
);
}
throw new FirebaseError(
`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`,
{ children: [build.error] },
Expand Down Expand Up @@ -137,7 +164,7 @@ function toBackend(cloudBuildConnRepo: Repository): Omit<Backend, BackendOutputO
repository: `${cloudBuildConnRepo.name}`,
rootDirectory: "/",
},
labels: {},
labels: deploymentTool.labels(),
};
}

Expand Down
5 changes: 3 additions & 2 deletions src/test/init/apphosting/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as poller from "../../../operation-poller";
import * as prompt from "../../../prompt";
import { createBackend, onboardBackend } from "../../../init/features/apphosting/index";
import { FirebaseError } from "../../../error";
import * as deploymentTool from "../../../deploymentTool";

describe("operationsConverter", () => {
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
Expand Down Expand Up @@ -66,10 +67,10 @@ describe("operationsConverter", () => {
repository: cloudBuildConnRepo.name,
rootDirectory: "/",
},
labels: {},
labels: deploymentTool.labels(),
};

it("should createBackend", async () => {
it("should createBackend with rollout policy", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nothing here tests to make sure the rollout policy was called. Please add the appropriate logic here to test for that

Copy link
Member Author

Choose a reason for hiding this comment

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

I may need to revert the label. I originally wrote the test to exercise this but realized it was for a smaller utility function and the larger flow is completely untested. I can either:

  1. Move creation of the rollout policy into a utility function and test it. This would be a trivial whitebox test that is unlikely to catch anything though.
  2. Add a new test that covers the overall flow of the entiere init process.
  3. Cry that init functions are generally sparsely tested in this codebase.

You make the call and I'll follow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, yeah, this isn't actually testing doSetup. Probably just revert the test name then. init function coverage is kinda a known weak point.

createBackendStub.resolves(op);
pollOperationStub.resolves(completeBackend);

Expand Down