@@ -12,6 +12,8 @@ import {
1212 Build ,
1313 Rollout ,
1414} from "../../../gcp/apphosting" ;
15+ import { addServiceAccountToRoles } from "../../../gcp/resourceManager" ;
16+ import { createServiceAccount } from "../../../gcp/iam" ;
1517import { Repository } from "../../../gcp/cloudbuild" ;
1618import { FirebaseError } from "../../../error" ;
1719import { promptOnce } from "../../../prompt" ;
@@ -20,6 +22,8 @@ import { ensure } from "../../../ensureApiEnabled";
2022import * as deploymentTool from "../../../deploymentTool" ;
2123import { DeepOmit } from "../../../metaprogramming" ;
2224
25+ const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute" ;
26+
2327const apphostingPollerOptions : Omit < poller . OperationPollerOptions , "operationResourceName" > = {
2428 apiOrigin : apphostingOrigin ,
2529 apiVersion : API_VERSION ,
@@ -30,7 +34,11 @@ const apphostingPollerOptions: Omit<poller.OperationPollerOptions, "operationRes
3034/**
3135 * Set up a new App Hosting backend.
3236 */
33- export async function doSetup ( projectId : string , location : string | null ) : Promise < void > {
37+ export async function doSetup (
38+ projectId : string ,
39+ location : string | null ,
40+ serviceAccount : string | null ,
41+ ) : Promise < void > {
3442 await Promise . all ( [
3543 ensure ( projectId , "cloudbuild.googleapis.com" , "apphosting" , true ) ,
3644 ensure ( projectId , "secretmanager.googleapis.com" , "apphosting" , true ) ,
@@ -73,7 +81,13 @@ export async function doSetup(projectId: string, location: string | null): Promi
7381
7482 const cloudBuildConnRepo = await repo . linkGitHubRepository ( projectId , location ) ;
7583
76- const backend = await createBackend ( projectId , location , backendId , cloudBuildConnRepo ) ;
84+ const backend = await createBackend (
85+ projectId ,
86+ location ,
87+ backendId ,
88+ cloudBuildConnRepo ,
89+ serviceAccount ,
90+ ) ;
7791
7892 // TODO: Once tag patterns are implemented, prompt which method the user
7993 // prefers. We could reduce the number of questions asked by letting people
@@ -137,31 +151,83 @@ async function promptNewBackendId(
137151 }
138152}
139153
154+ function defaultComputeServiceAccountEmail ( projectId : string ) : string {
155+ return `${ DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME } @${ projectId } .iam.gserviceaccount.com` ;
156+ }
157+
140158/**
141- * Creates (and waits for) a new backend.
159+ * Creates (and waits for) a new backend. Optionally may create the default compute service account if
160+ * it was requested and doesn't exist.
142161 */
143162export async function createBackend (
144163 projectId : string ,
145164 location : string ,
146165 backendId : string ,
147166 repository : Repository ,
167+ serviceAccount : string | null ,
148168) : Promise < Backend > {
169+ const defaultServiceAccount = defaultComputeServiceAccountEmail ( projectId ) ;
149170 const backendReqBody : Omit < Backend , BackendOutputOnlyFields > = {
150171 servingLocality : "GLOBAL_ACCESS" ,
151172 codebase : {
152173 repository : `${ repository . name } ` ,
153174 rootDirectory : "/" ,
154175 } ,
155176 labels : deploymentTool . labels ( ) ,
177+ computeServiceAccount : serviceAccount || defaultServiceAccount ,
156178 } ;
157179
158- const op = await apphosting . createBackend ( projectId , location , backendReqBody , backendId ) ;
159- const backend = await poller . pollOperation < Backend > ( {
160- ...apphostingPollerOptions ,
161- pollerName : `create-${ projectId } -${ location } -${ backendId } ` ,
162- operationResourceName : op . name ,
163- } ) ;
164- return backend ;
180+ // TODO: remove computeServiceAccount when the backend supports the field.
181+ delete backendReqBody . computeServiceAccount ;
182+
183+ async function createBackendAndPoll ( ) {
184+ const op = await apphosting . createBackend ( projectId , location , backendReqBody , backendId ) ;
185+ return await poller . pollOperation < Backend > ( {
186+ ...apphostingPollerOptions ,
187+ pollerName : `create-${ projectId } -${ location } -${ backendId } ` ,
188+ operationResourceName : op . name ,
189+ } ) ;
190+ }
191+
192+ try {
193+ return await createBackendAndPoll ( ) ;
194+ } catch ( err : any ) {
195+ if ( err . status === 403 && err . message . includes ( defaultServiceAccount ) ) {
196+ // Create the default service account if it doesn't exist and try again.
197+ await provisionDefaultComputeServiceAccount ( projectId ) ;
198+ return await createBackendAndPoll ( ) ;
199+ }
200+ throw err ;
201+ }
202+ }
203+
204+ async function provisionDefaultComputeServiceAccount ( projectId : string ) : Promise < void > {
205+ try {
206+ await createServiceAccount (
207+ projectId ,
208+ DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME ,
209+ "Firebase App Hosting compute service account" ,
210+ "Default service account used to run builds and deploys for Firebase App Hosting" ,
211+ ) ;
212+ } catch ( err : any ) {
213+ // 409 Already Exists errors can safely be ignored.
214+ if ( err . status !== 409 ) {
215+ throw err ;
216+ }
217+ }
218+ await addServiceAccountToRoles (
219+ projectId ,
220+ defaultComputeServiceAccountEmail ( projectId ) ,
221+ [
222+ // TODO: Update to roles/firebaseapphosting.computeRunner when it is available.
223+ "roles/firebaseapphosting.viewer" ,
224+ "roles/artifactregistry.createOnPushWriter" ,
225+ "roles/logging.logWriter" ,
226+ "roles/storage.objectAdmin" ,
227+ "roles/firebase.sdkAdminServiceAgent" ,
228+ ] ,
229+ /* skipAccountLookup= */ true ,
230+ ) ;
165231}
166232
167233/**
0 commit comments