Skip to content

Commit 0f59e99

Browse files
authored
Provision App Hosting compute service account during init flow (#8580)
* provision compute SA in fah init flow
1 parent d97911a commit 0f59e99

File tree

5 files changed

+41
-14
lines changed

5 files changed

+41
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
- Add GCP API client functions to support App Hosting deploy from source feature. (#8545)
66
- Changed firebase init template for functions to pin runtime version on init. (#8553)
77
- Fix an issue where updating a Cloud Function that retires would add incorrect fields to the updateMask. (#8560)
8+
- Provision App Hosting compute service account during init flow. (#8580)

src/apphosting/backend.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,7 @@ export async function doSetup(
7676
webAppName: string | null,
7777
serviceAccount: string | null,
7878
): Promise<void> {
79-
await Promise.all([
80-
ensure(projectId, developerConnectOrigin(), "apphosting", true),
81-
ensure(projectId, cloudbuildOrigin(), "apphosting", true),
82-
ensure(projectId, secretManagerOrigin(), "apphosting", true),
83-
ensure(projectId, cloudRunApiOrigin(), "apphosting", true),
84-
ensure(projectId, artifactRegistryDomain(), "apphosting", true),
85-
ensure(projectId, iamOrigin(), "apphosting", true),
86-
]);
79+
await ensureRequiredApisEnabled(projectId);
8780

8881
// Hack: Because IAM can take ~45 seconds to propagate, we provision the service account as soon as
8982
// possible to reduce the likelihood that the subsequent Cloud Build fails. See b/336862200.
@@ -179,6 +172,20 @@ export async function doSetup(
179172
logSuccess(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
180173
}
181174

175+
/**
176+
* Check that all GCP APIs required for App Hosting are enabled.
177+
*/
178+
export async function ensureRequiredApisEnabled(projectId: string): Promise<void> {
179+
await Promise.all([
180+
ensure(projectId, developerConnectOrigin(), "apphosting", true),
181+
ensure(projectId, cloudbuildOrigin(), "apphosting", true),
182+
ensure(projectId, secretManagerOrigin(), "apphosting", true),
183+
ensure(projectId, cloudRunApiOrigin(), "apphosting", true),
184+
ensure(projectId, artifactRegistryDomain(), "apphosting", true),
185+
ensure(projectId, iamOrigin(), "apphosting", true),
186+
]);
187+
}
188+
182189
/**
183190
* Set up a new App Hosting-type Developer Connect GitRepoLink, optionally with a specific connection ID
184191
*/

src/commands/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ let choices: {
4848
},
4949
{
5050
value: "apphosting",
51-
name: "App Hosting: Configure an apphosting.yaml file for App Hosting",
51+
name: "App Hosting: Enable web app deployments with App Hosting",
5252
checked: false,
5353
hidden: false,
5454
},

src/init/features/apphosting.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@ import * as clc from "colorette";
22
import { existsSync } from "fs";
33
import * as ora from "ora";
44
import * as path from "path";
5+
import { Setup } from "..";
56
import { webApps } from "../../apphosting/app";
67
import {
78
createBackend,
9+
ensureAppHostingComputeServiceAccount,
10+
ensureRequiredApisEnabled,
811
promptExistingBackend,
912
promptLocation,
1013
promptNewBackendId,
1114
} from "../../apphosting/backend";
1215
import { Config } from "../../config";
1316
import { FirebaseError } from "../../error";
1417
import { AppHostingSingle } from "../../firebaseConfig";
18+
import { ensureApiEnabled } from "../../gcp/apphosting";
19+
import { isBillingEnabled } from "../../gcp/cloudbilling";
20+
import { input, select } from "../../prompt";
1521
import { readTemplateSync } from "../../templates";
1622
import * as utils from "../../utils";
1723
import { logBullet } from "../../utils";
18-
import { input, select } from "../../prompt";
19-
import { Setup } from "..";
20-
import { isBillingEnabled } from "../../gcp/cloudbilling";
2124

2225
const APPHOSTING_YAML_TEMPLATE = readTemplateSync("init/apphosting/apphosting.yaml");
2326

@@ -31,6 +34,19 @@ export async function doSetup(setup: Setup, config: Config): Promise<void> {
3134
"Firebase App Hosting requires billing to be enabled on your project. Please enable billing by following the steps at https://cloud.google.com/billing/docs/how-to/modify-project",
3235
);
3336
}
37+
await ensureApiEnabled({ projectId });
38+
await ensureRequiredApisEnabled(projectId);
39+
try {
40+
await ensureAppHostingComputeServiceAccount(projectId, /* serviceAccount= */ "");
41+
} catch (err) {
42+
if ((err as FirebaseError).status === 400) {
43+
utils.logWarning(
44+
"Your App Hosting compute service account is still being provisioned. Please try again in a few moments.",
45+
);
46+
}
47+
throw err;
48+
}
49+
3450
utils.logBullet(
3551
"This command links your local project to Firebase App Hosting. You will be able to deploy your web app with `firebase deploy` after setup.",
3652
);

src/init/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface SetupInfo {
3131

3232
interface Feature {
3333
name: string;
34+
displayName?: string;
3435
// OLD WAY: A single setup function to ask questions and actuate the setup.
3536
doSetup?: (setup: Setup, config: Config, options: Options) => Promise<unknown>;
3637

@@ -64,7 +65,7 @@ const featuresList: Feature[] = [
6465
{ name: "remoteconfig", doSetup: features.remoteconfig },
6566
{ name: "hosting:github", doSetup: features.hostingGithub },
6667
{ name: "genkit", doSetup: features.genkit },
67-
{ name: "apphosting", doSetup: features.apphosting },
68+
{ name: "apphosting", displayName: "App Hosting", doSetup: features.apphosting },
6869
];
6970

7071
const featureMap = new Map(featuresList.map((feature) => [feature.name, feature]));
@@ -73,7 +74,9 @@ export async function init(setup: Setup, config: any, options: any): Promise<any
7374
const nextFeature = setup.features?.shift();
7475
if (nextFeature) {
7576
const f = lookupFeature(nextFeature);
76-
logger.info(clc.bold(`\n${clc.white("===")} ${capitalize(nextFeature)} Setup`));
77+
logger.info(
78+
clc.bold(`\n${clc.white("===")} ${f.displayName || capitalize(nextFeature)} Setup`),
79+
);
7780

7881
if (f.doSetup) {
7982
await f.doSetup(setup, config, options);

0 commit comments

Comments
 (0)