Skip to content

Commit 2df26c0

Browse files
authored
Light control flow cleanups within backends:create (#6775)
* refactor * naming * merge * move prompt back into main control flow
1 parent 01c01b0 commit 2df26c0

File tree

3 files changed

+136
-129
lines changed

3 files changed

+136
-129
lines changed

src/commands/apphosting-backends-create.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export const command = new Command("apphosting:backends:create")
1212
.before(requireInteractive)
1313
.action(async (options: Options) => {
1414
const projectId = needProjectId(options);
15-
await doSetup(options, projectId);
15+
const location = options.location;
16+
await doSetup(projectId, location as string | null);
1617
});

src/init/features/apphosting/index.ts

Lines changed: 101 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const apphostingPollerOptions: Omit<poller.OperationPollerOptions, "operationRes
2929
/**
3030
* Set up a new App Hosting backend.
3131
*/
32-
export async function doSetup(setup: any, projectId: string): Promise<void> {
32+
export async function doSetup(projectId: string, location: string | null): Promise<void> {
3333
await Promise.all([
3434
ensure(projectId, "cloudbuild.googleapis.com", "apphosting", true),
3535
ensure(projectId, "secretmanager.googleapis.com", "apphosting", true),
@@ -39,70 +39,52 @@ export async function doSetup(setup: any, projectId: string): Promise<void> {
3939

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

42-
if (setup.location) {
43-
if (!allowedLocations.includes(setup.location)) {
42+
if (location) {
43+
if (!allowedLocations.includes(location)) {
4444
throw new FirebaseError(
45-
`Invalid location ${setup.location}. Valid choices are ${allowedLocations.join(", ")}`,
45+
`Invalid location ${location}. Valid choices are ${allowedLocations.join(", ")}`,
4646
);
4747
}
4848
}
4949

5050
logBullet("First we need a few details to create your backend.");
5151

52-
const location: string = setup.location || (await promptLocation(projectId, allowedLocations));
52+
location =
53+
location ||
54+
((await promptOnce({
55+
name: "region",
56+
type: "list",
57+
default: DEFAULT_REGION,
58+
message:
59+
"Please select a region " +
60+
`(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
61+
choices: allowedLocations.map((loc) => ({ value: loc })),
62+
})) as string);
5363

5464
logSuccess(`Region set to ${location}.\n`);
5565

56-
let backendId: string;
57-
while (true) {
58-
backendId = await promptOnce({
59-
name: "backendId",
60-
type: "input",
61-
default: "my-web-app",
62-
message: "Create a name for your backend [1-30 characters]",
63-
});
64-
try {
65-
await apphosting.getBackend(projectId, location, backendId);
66-
} catch (err: any) {
67-
if (err.status === 404) {
68-
break;
69-
}
70-
throw new FirebaseError(
71-
`Failed to check if backend with id ${backendId} already exists in ${location}`,
72-
{ original: err },
73-
);
74-
}
75-
logWarning(`Backend with id ${backendId} already exists in ${location}`);
76-
}
66+
const backendId = await promptNewBackendId(projectId, location, {
67+
name: "backendId",
68+
type: "input",
69+
default: "my-web-app",
70+
message: "Create a name for your backend [1-30 characters]",
71+
});
7772

78-
const backend = await onboardBackend(projectId, location, backendId);
73+
const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
74+
75+
const backend = await createBackend(projectId, location, backendId, cloudBuildConnRepo);
7976

8077
// TODO: Once tag patterns are implemented, prompt which method the user
81-
// prefers. We could reduce the nubmer of questions asked by letting people
78+
// prefers. We could reduce the number of questions asked by letting people
8279
// enter tag:<pattern>?
8380
const branch = await promptOnce({
8481
name: "branch",
8582
type: "input",
8683
default: "main",
8784
message: "Pick a branch for continuous deployment",
8885
});
89-
const traffic: DeepOmit<apphosting.Traffic, apphosting.TrafficOutputOnlyFields | "name"> = {
90-
rolloutPolicy: {
91-
codebaseBranch: branch,
92-
stages: [
93-
{
94-
progression: "IMMEDIATE",
95-
targetPercent: 100,
96-
},
97-
],
98-
},
99-
};
100-
const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
101-
await poller.pollOperation<apphosting.Traffic>({
102-
...apphostingPollerOptions,
103-
pollerName: `updateTraffic-${projectId}-${location}-${backendId}`,
104-
operationResourceName: op.name,
105-
});
86+
87+
await setDefaultTrafficPolicy(projectId, location, backendId, branch);
10688

10789
const confirmRollout = await promptOnce({
10890
type: "confirm",
@@ -117,71 +99,41 @@ export async function doSetup(setup: any, projectId: string): Promise<void> {
11799
return;
118100
}
119101

120-
const { build } = await onboardRollout(projectId, location, backendId, {
102+
await orchestrateRollout(projectId, location, backendId, {
121103
source: {
122104
codebase: {
123105
branch,
124106
},
125107
},
126108
});
127109

128-
if (build.state !== "READY") {
129-
if (!build.buildLogsUri) {
130-
throw new FirebaseError(
131-
"Failed to build your app, but failed to get build logs as well. " +
132-
"This is an internal error and should be reported",
133-
);
134-
}
135-
throw new FirebaseError(
136-
`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`,
137-
{ children: [build.error] },
138-
);
139-
}
140-
141110
logSuccess(`Successfully created backend:\n\t${backend.name}`);
142111
logSuccess(`Your site is now deployed at:\n\thttps://${backend.uri}`);
143-
logSuccess(
144-
`View the rollout status by running:\n\tfirebase apphosting:backends:get ${backendId} --project ${projectId}`,
145-
);
146-
logSuccess(
147-
`See your new App Hosting backend in the Firebase console at:\n\thttps://console.firebase.google.com/project/${projectId}/apphosting`,
148-
);
149-
}
150-
151-
async function promptLocation(projectId: string, locations: string[]): Promise<string> {
152-
return (await promptOnce({
153-
name: "region",
154-
type: "list",
155-
default: DEFAULT_REGION,
156-
message:
157-
"Please select a region " +
158-
`(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
159-
choices: locations.map((loc) => ({ value: loc })),
160-
})) as string;
161-
}
162-
163-
function toBackend(cloudBuildConnRepo: Repository): DeepOmit<Backend, BackendOutputOnlyFields> {
164-
return {
165-
servingLocality: "GLOBAL_ACCESS",
166-
codebase: {
167-
repository: `${cloudBuildConnRepo.name}`,
168-
rootDirectory: "/",
169-
},
170-
labels: deploymentTool.labels(),
171-
};
172112
}
173113

174114
/**
175-
* Walkthrough the flow for creating a new backend.
115+
* Prompts the user for a backend id and verifies that it doesn't match a pre-existing backend.
176116
*/
177-
export async function onboardBackend(
117+
async function promptNewBackendId(
178118
projectId: string,
179119
location: string,
180-
backendId: string,
181-
): Promise<Backend> {
182-
const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
183-
const backendDetails = toBackend(cloudBuildConnRepo);
184-
return await createBackend(projectId, location, backendDetails, backendId);
120+
prompt: any,
121+
): Promise<string> {
122+
while (true) {
123+
const backendId = await promptOnce(prompt);
124+
try {
125+
await apphosting.getBackend(projectId, location, backendId);
126+
} catch (err: any) {
127+
if (err.status === 404) {
128+
return backendId;
129+
}
130+
throw new FirebaseError(
131+
`Failed to check if backend with id ${backendId} already exists in ${location}`,
132+
{ original: err },
133+
);
134+
}
135+
logWarning(`Backend with id ${backendId} already exists in ${location}`);
136+
}
185137
}
186138

187139
/**
@@ -190,10 +142,19 @@ export async function onboardBackend(
190142
export async function createBackend(
191143
projectId: string,
192144
location: string,
193-
backendReqBoby: Omit<Backend, BackendOutputOnlyFields>,
194145
backendId: string,
146+
repository: Repository,
195147
): Promise<Backend> {
196-
const op = await apphosting.createBackend(projectId, location, backendReqBoby, backendId);
148+
const backendReqBody: Omit<Backend, BackendOutputOnlyFields> = {
149+
servingLocality: "GLOBAL_ACCESS",
150+
codebase: {
151+
repository: `${repository.name}`,
152+
rootDirectory: "/",
153+
},
154+
labels: deploymentTool.labels(),
155+
};
156+
157+
const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
197158
const backend = await poller.pollOperation<Backend>({
198159
...apphostingPollerOptions,
199160
pollerName: `create-${projectId}-${location}-${backendId}`,
@@ -203,9 +164,37 @@ export async function createBackend(
203164
}
204165

205166
/**
206-
* Onboard a new rollout by creating a build and rollout
167+
* Sets the default rollout policy to route 100% of traffic to the latest deploy.
168+
*/
169+
export async function setDefaultTrafficPolicy(
170+
projectId: string,
171+
location: string,
172+
backendId: string,
173+
codebaseBranch: string,
174+
): Promise<void> {
175+
const traffic: DeepOmit<apphosting.Traffic, apphosting.TrafficOutputOnlyFields | "name"> = {
176+
rolloutPolicy: {
177+
codebaseBranch: codebaseBranch,
178+
stages: [
179+
{
180+
progression: "IMMEDIATE",
181+
targetPercent: 100,
182+
},
183+
],
184+
},
185+
};
186+
const op = await apphosting.updateTraffic(projectId, location, backendId, traffic);
187+
await poller.pollOperation<apphosting.Traffic>({
188+
...apphostingPollerOptions,
189+
pollerName: `updateTraffic-${projectId}-${location}-${backendId}`,
190+
operationResourceName: op.name,
191+
});
192+
}
193+
194+
/**
195+
* Creates a new build and rollout and polls both to completion.
207196
*/
208-
export async function onboardRollout(
197+
export async function orchestrateRollout(
209198
projectId: string,
210199
location: string,
211200
backendId: string,
@@ -232,5 +221,18 @@ export async function onboardRollout(
232221

233222
const [rollout, build] = await Promise.all([rolloutPoll, buildPoll]);
234223
logSuccess("Rollout completed.");
224+
225+
if (build.state !== "READY") {
226+
if (!build.buildLogsUri) {
227+
throw new FirebaseError(
228+
"Failed to build your app, but failed to get build logs as well. " +
229+
"This is an internal error and should be reported",
230+
);
231+
}
232+
throw new FirebaseError(
233+
`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`,
234+
{ children: [build.error] },
235+
);
236+
}
235237
return { rollout, build };
236238
}

src/test/init/apphosting/index.spec.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@ import * as sinon from "sinon";
22
import { expect } from "chai";
33

44
import * as apphosting from "../../../gcp/apphosting";
5-
import * as repo from "../../../init/features/apphosting/repo";
65
import * as poller from "../../../operation-poller";
7-
import * as prompt from "../../../prompt";
8-
import { createBackend, onboardBackend } from "../../../init/features/apphosting/index";
9-
import { FirebaseError } from "../../../error";
6+
import { createBackend, setDefaultTrafficPolicy } from "../../../init/features/apphosting/index";
107
import * as deploymentTool from "../../../deploymentTool";
118

129
describe("operationsConverter", () => {
1310
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
1411

1512
let pollOperationStub: sinon.SinonStub;
1613
let createBackendStub: sinon.SinonStub;
17-
let getBackendStub: sinon.SinonStub;
18-
let linkGitHubRepositoryStub: sinon.SinonStub;
19-
let promptOnce: sinon.SinonStub;
14+
let updateTrafficStub: sinon.SinonStub;
2015

2116
beforeEach(() => {
2217
pollOperationStub = sandbox
@@ -25,11 +20,9 @@ describe("operationsConverter", () => {
2520
createBackendStub = sandbox
2621
.stub(apphosting, "createBackend")
2722
.throws("Unexpected createBackend call");
28-
getBackendStub = sandbox.stub(apphosting, "getBackend").throws("Unexpected getBackend call");
29-
linkGitHubRepositoryStub = sandbox
30-
.stub(repo, "linkGitHubRepository")
31-
.throws("Unexpected getBackend call");
32-
promptOnce = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call");
23+
updateTrafficStub = sandbox
24+
.stub(apphosting, "updateTraffic")
25+
.throws("Unexpected updateTraffic call");
3326
});
3427

3528
afterEach(() => {
@@ -70,30 +63,41 @@ describe("operationsConverter", () => {
7063
labels: deploymentTool.labels(),
7164
};
7265

73-
it("should createBackend", async () => {
66+
it("should create a new backend", async () => {
7467
createBackendStub.resolves(op);
7568
pollOperationStub.resolves(completeBackend);
7669

77-
await createBackend(projectId, location, backendInput, backendId);
70+
await createBackend(projectId, location, backendId, cloudBuildConnRepo);
7871

7972
expect(createBackendStub).to.be.calledWith(projectId, location, backendInput);
8073
});
8174

82-
it("should onboard a new backend", async () => {
83-
const newBackendId = "newBackendId";
84-
const newPath = `projects/${projectId}/locations/${location}/backends/${newBackendId}`;
85-
op.name = newPath;
86-
completeBackend.name = newPath;
87-
getBackendStub.throws(new FirebaseError("error", { status: 404 }));
88-
linkGitHubRepositoryStub.resolves(cloudBuildConnRepo);
89-
createBackendStub.resolves(op);
90-
pollOperationStub.resolves(completeBackend);
91-
promptOnce.resolves("main");
92-
93-
const result = await onboardBackend(projectId, location, backendId);
94-
95-
expect(result).to.deep.equal(completeBackend);
96-
expect(createBackendStub).to.be.calledWith(projectId, location, backendInput);
75+
it("should set default rollout policy to 100% all at once", async () => {
76+
const completeTraffic: apphosting.Traffic = {
77+
name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`,
78+
current: { splits: [] },
79+
reconciling: false,
80+
createTime: "0",
81+
updateTime: "1",
82+
etag: "",
83+
uid: "",
84+
};
85+
updateTrafficStub.resolves(op);
86+
pollOperationStub.resolves(completeTraffic);
87+
88+
await setDefaultTrafficPolicy(projectId, location, backendId, "main");
89+
90+
expect(updateTrafficStub).to.be.calledWith(projectId, location, backendId, {
91+
rolloutPolicy: {
92+
codebaseBranch: "main",
93+
stages: [
94+
{
95+
progression: "IMMEDIATE",
96+
targetPercent: 100,
97+
},
98+
],
99+
},
100+
});
97101
});
98102
});
99103
});

0 commit comments

Comments
 (0)