Skip to content

Commit 740da34

Browse files
authored
Improve App Hosting compute service account flow for source deploys (#8785)
* improve app hosting compute SA flow for source deploys * move try catch closer to error source in provisioning app hosting sa
1 parent 87cc755 commit 740da34

File tree

5 files changed

+41
-64
lines changed

5 files changed

+41
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Improve App Hosting compute service account flow for source deploys. (#8785)

src/apphosting/backend.spec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,24 @@ describe("apphosting setup functions", () => {
152152

153153
it("should succeed if the user has permissions for the service account", async () => {
154154
testResourceIamPermissionsStub.resolves();
155+
createServiceAccountStub.resolves();
156+
addServiceAccountToRolesStub.resolves();
155157

156158
await expect(ensureAppHostingComputeServiceAccount(projectId, serviceAccount)).to.be
157159
.fulfilled;
158160

159161
expect(testResourceIamPermissionsStub).to.be.calledOnce;
160-
expect(createServiceAccountStub).to.not.be.called;
161-
expect(addServiceAccountToRolesStub).to.not.be.called;
162+
});
163+
164+
it("should still add permissions even if the service account already exists", async () => {
165+
testResourceIamPermissionsStub.resolves();
166+
createServiceAccountStub.rejects(new FirebaseError("error occurred", { status: 409 }));
167+
addServiceAccountToRolesStub.resolves();
168+
169+
await expect(ensureAppHostingComputeServiceAccount(projectId, serviceAccount)).to.be
170+
.fulfilled;
171+
172+
expect(addServiceAccountToRolesStub).to.be.calledOnce;
162173
});
163174

164175
it("should succeed if the user can create the service account when it does not exist", async () => {

src/apphosting/backend.ts

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
secretManagerOrigin,
1515
} from "../api";
1616
import { Backend, BackendOutputOnlyFields, API_VERSION } from "../gcp/apphosting";
17-
import { addServiceAccountToRoles, getIamPolicy } from "../gcp/resourceManager";
17+
import { addServiceAccountToRoles } from "../gcp/resourceManager";
1818
import * as iam from "../gcp/iam";
1919
import { FirebaseError, getErrStatus, getError } from "../error";
2020
import { input, confirm, select, checkbox, search, Choice } from "../prompt";
@@ -246,13 +246,12 @@ export async function createGitRepoLink(
246246
/**
247247
* Ensures the service account is present the user has permissions to use it by
248248
* checking the `iam.serviceAccounts.actAs` permission. If the permissions
249-
* check fails, this returns an error. If the permission check fails with a
250-
* "not found" error, this attempts to provision the service account.
249+
* check fails, this returns an error. Otherwise, it attempts to provision the
250+
* service account.
251251
*/
252252
export async function ensureAppHostingComputeServiceAccount(
253253
projectId: string,
254254
serviceAccount: string | null,
255-
deployFromSource = false,
256255
): Promise<void> {
257256
const sa = serviceAccount || defaultComputeServiceAccountEmail(projectId);
258257
const name = `projects/${projectId}/serviceAccounts/${sa}`;
@@ -267,39 +266,14 @@ export async function ensureAppHostingComputeServiceAccount(
267266
} catch (err: unknown) {
268267
if (!(err instanceof FirebaseError)) {
269268
throw err;
270-
}
271-
if (err.status === 404) {
272-
await provisionDefaultComputeServiceAccount(projectId);
273269
} else if (err.status === 403) {
274270
throw new FirebaseError(
275271
`Failed to create backend due to missing delegation permissions for ${sa}. Make sure you have the iam.serviceAccounts.actAs permission.`,
276272
{ original: err },
277273
);
278274
}
279275
}
280-
// N.B. To deploy from source, the App Hosting Compute Service Account must have
281-
// the storage.objectViewer IAM role. For firebase-tools <= 14.3.0, the CLI does
282-
// not add the objectViewer role, which means all existing customers will need to
283-
// add it before deploying from source.
284-
if (deployFromSource) {
285-
const policy = await getIamPolicy(projectId);
286-
const objectViewerBinding = policy.bindings.find(
287-
(binding) => binding.role === "roles/storage.objectViewer",
288-
);
289-
if (
290-
!objectViewerBinding ||
291-
!objectViewerBinding.members.includes(
292-
`serviceAccount:${defaultComputeServiceAccountEmail(projectId)}`,
293-
)
294-
) {
295-
await addServiceAccountToRoles(
296-
projectId,
297-
defaultComputeServiceAccountEmail(projectId),
298-
["roles/storage.objectViewer"],
299-
/* skipAccountLookup= */ true,
300-
);
301-
}
302-
}
276+
await provisionDefaultComputeServiceAccount(projectId);
303277
}
304278

305279
/**
@@ -384,17 +358,27 @@ async function provisionDefaultComputeServiceAccount(projectId: string): Promise
384358
throw err;
385359
}
386360
}
387-
await addServiceAccountToRoles(
388-
projectId,
389-
defaultComputeServiceAccountEmail(projectId),
390-
[
391-
"roles/firebaseapphosting.computeRunner",
392-
"roles/firebase.sdkAdminServiceAgent",
393-
"roles/developerconnect.readTokenAccessor",
394-
"roles/storage.objectViewer",
395-
],
396-
/* skipAccountLookup= */ true,
397-
);
361+
try {
362+
await addServiceAccountToRoles(
363+
projectId,
364+
defaultComputeServiceAccountEmail(projectId),
365+
[
366+
"roles/firebaseapphosting.computeRunner",
367+
"roles/firebase.sdkAdminServiceAgent",
368+
"roles/developerconnect.readTokenAccessor",
369+
"roles/storage.objectViewer",
370+
],
371+
/* skipAccountLookup= */ true,
372+
);
373+
} catch (err: unknown) {
374+
if (getErrStatus(err) === 400) {
375+
logWarning(
376+
"Your App Hosting compute service account is still being provisioned in the background; you may continue with the init flow.",
377+
);
378+
} else {
379+
throw err;
380+
}
381+
}
398382
}
399383

400384
/**

src/deploy/apphosting/prepare.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ export default async function (context: Context, options: Options): Promise<void
2323
await ensureApiEnabled(options);
2424
await ensureRequiredApisEnabled(projectId);
2525
try {
26-
await ensureAppHostingComputeServiceAccount(
27-
projectId,
28-
/* serviceAccount= */ "",
29-
/* deployFromSource= */ true,
30-
);
26+
await ensureAppHostingComputeServiceAccount(projectId, /* serviceAccount= */ "");
3127
} catch (err) {
3228
if ((err as FirebaseError).status === 400) {
3329
logLabeledWarning(

src/init/features/apphosting.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,7 @@ export async function doSetup(setup: Setup, config: Config): Promise<void> {
4343
// since IAM propagation delay will likely cause the first one to fail. However,
4444
// `firebase init apphosting` is a prerequisite to the `firebase deploy` command,
4545
// so we check and add the role here to give the IAM changes time to propagate.
46-
const spinner = ora("Checking your App Hosting compute service account...").start();
47-
try {
48-
await ensureAppHostingComputeServiceAccount(
49-
projectId,
50-
/* serviceAccount= */ "",
51-
/* deployFromSource= */ true,
52-
);
53-
spinner.succeed("App Hosting compute Service account is ready");
54-
} catch (err) {
55-
if ((err as FirebaseError).status === 400) {
56-
spinner.warn(
57-
"Your App Hosting compute service account is still being provisioned. Please try again in a few moments.",
58-
);
59-
}
60-
throw err;
61-
}
46+
await ensureAppHostingComputeServiceAccount(projectId, /* serviceAccount= */ "");
6247

6348
utils.logBullet(
6449
"This command links your local project to Firebase App Hosting. You will be able to deploy your web app with `firebase deploy` after setup.",

0 commit comments

Comments
 (0)