diff --git a/spec/function-builder.spec.ts b/spec/function-builder.spec.ts index eec531fdf..c31a82d4a 100644 --- a/spec/function-builder.spec.ts +++ b/spec/function-builder.spec.ts @@ -238,4 +238,52 @@ describe('FunctionBuilder', () => { )}` ); }); + + it('should allow a serviceAccount to be set as-is', () => { + const serviceAccount = 'test-service-account@test.iam.gserviceaccount.com'; + const fn = functions + .runWith({ + serviceAccount, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.serviceAccountEmail).to.equal(serviceAccount); + }); + + it('should allow a serviceAccount to be set with generated service account email', () => { + const serviceAccount = 'test-service-account@'; + const projectId = process.env.GCLOUD_PROJECT; + const fn = functions + .runWith({ + serviceAccount, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.serviceAccountEmail).to.equal( + `test-service-account@${projectId}.iam.gserviceaccount.com` + ); + }); + + it('should not set a serviceAccountEmail if service account is set to `default`', () => { + const serviceAccount = 'default'; + const fn = functions + .runWith({ + serviceAccount, + }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.serviceAccountEmail).to.be.undefined; + }); + + it('should throw an error if serviceAccount is set to an invalid value', () => { + const serviceAccount = 'test-service-account'; + expect(() => { + functions.runWith({ + serviceAccount, + }); + }).to.throw(); + }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 109a1ad05..50d674022 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -272,6 +272,7 @@ export interface TriggerAnnotated { timeout?: string; vpcConnector?: string; vpcConnectorEgressSettings?: string; + serviceAccountEmail?: string; }; } @@ -525,5 +526,24 @@ export function optionsToTrigger(options: DeploymentOptions) { trigger.vpcConnectorEgressSettings = options.vpcConnectorEgressSettings; } + if (options.serviceAccount) { + if (options.serviceAccount === 'default') { + // Do nothing, since this is equivalent to not setting serviceAccount. + } else if (options.serviceAccount.endsWith('@')) { + if (!process.env.GCLOUD_PROJECT) { + throw new Error( + `Unable to determine email for service account '${options.serviceAccount}' because process.env.GCLOUD_PROJECT is not set.` + ); + } + trigger.serviceAccountEmail = `${options.serviceAccount}${process.env.GCLOUD_PROJECT}.iam.gserviceaccount.com`; + } else if (options.serviceAccount.includes('@')) { + trigger.serviceAccountEmail = options.serviceAccount; + } else { + throw new Error( + `Invalid option for serviceAccount: '${options.serviceAccount}'. Valid options are 'default', a service account email, or '{serviceAccountName}@'` + ); + } + } + return trigger; } diff --git a/src/function-builder.ts b/src/function-builder.ts index 9b5f5660b..7411f72e2 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -99,6 +99,16 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } } } + + if ( + runtimeOptions.serviceAccount && + runtimeOptions.serviceAccount !== 'default' && + !_.includes(runtimeOptions.serviceAccount, '@') + ) { + throw new Error( + `serviceAccount must be set to 'default', a service account email, or '{serviceAccountName}@'` + ); + } return true; } @@ -139,9 +149,12 @@ export function region( * 0 to 540. * 3. `failurePolicy`: failure policy of the function, with boolean `true` being * equivalent to providing an empty retry object. - * 4. `vpcConnector`: id of a VPC connector in the same project and region - * 5. `vpcConnectorEgressSettings`: when a `vpcConnector` is set, control which - * egress traffic is sent through the `vpcConnector`. + * 4. `vpcConnector`: id of a VPC connector in same project and region. + * 5. `vpcConnectorEgressSettings`: when a vpcConnector is set, control which + * egress traffic is sent through the vpcConnector. + * 6. `serviceAccount`: Specific service account for the function. + * 7. `ingressSettings`: ingress settings for the function, which control where a HTTPS + * function can be called from. * * Value must not be null. */ diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 10098a745..f5a325b41 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -122,6 +122,11 @@ export interface RuntimeOptions { */ vpcConnectorEgressSettings?: typeof VPC_EGRESS_SETTINGS_OPTIONS[number]; + /** + * Specific service account for the function to run as + */ + serviceAccount?: 'default' | string; + /** * Ingress settings */