diff --git a/src/deploy/functions/services/index.ts b/src/deploy/functions/services/index.ts index ba09c9bfa5a..3009175ca77 100644 --- a/src/deploy/functions/services/index.ts +++ b/src/deploy/functions/services/index.ts @@ -6,6 +6,7 @@ import { obtainStorageBindings, ensureStorageTriggerRegion } from "./storage"; import { ensureFirebaseAlertsTriggerRegion } from "./firebaseAlerts"; import { ensureDatabaseTriggerRegion } from "./database"; import { ensureRemoteConfigTriggerRegion } from "./remoteConfig"; +import { ensureTestLabTriggerRegion } from "./testLab"; /** A standard void No Op */ export const noop = (): Promise => Promise.resolve(); @@ -21,7 +22,8 @@ export type Name = | "firebasealerts" | "authblocking" | "database" - | "remoteconfig"; + | "remoteconfig" + | "testlab"; /** A service interface for the underlying GCP event services */ export interface Service { @@ -104,6 +106,17 @@ const remoteConfigService: Service = { unregisterTrigger: noop, }; +/** A test lab service object */ +const testLabService: Service = { + name: "testlab", + api: "testing.googleapis.com", + requiredProjectBindings: noopProjectBindings, + ensureTriggerRegion: ensureTestLabTriggerRegion, + validateTrigger: noop, + registerTrigger: noop, + unregisterTrigger: noop, +}; + /** Mapping from event type string to service object */ const EVENT_SERVICE_MAPPING: Record = { "google.cloud.pubsub.topic.v1.messagePublished": pubSubService, @@ -119,6 +132,7 @@ const EVENT_SERVICE_MAPPING: Record = { "google.firebase.database.ref.v1.updated": databaseService, "google.firebase.database.ref.v1.deleted": databaseService, "google.firebase.remoteconfig.remoteConfig.v1.updated": remoteConfigService, + "google.firebase.testlab.testMatrix.v1.completed": testLabService, }; /** diff --git a/src/deploy/functions/services/testLab.ts b/src/deploy/functions/services/testLab.ts new file mode 100644 index 00000000000..a4f76503350 --- /dev/null +++ b/src/deploy/functions/services/testLab.ts @@ -0,0 +1,18 @@ +import * as backend from "../backend"; +import { FirebaseError } from "../../../error"; + +/** + * Sets a Test Lab event trigger's region to 'global' since the service is global + * @param endpoint the test lab endpoint + */ +export function ensureTestLabTriggerRegion( + endpoint: backend.Endpoint & backend.EventTriggered +): Promise { + if (!endpoint.eventTrigger.region) { + endpoint.eventTrigger.region = "global"; + } + if (endpoint.eventTrigger.region !== "global") { + throw new FirebaseError("A test lab trigger must specify 'global' trigger location"); + } + return Promise.resolve(); +} diff --git a/src/functions/events/v2.ts b/src/functions/events/v2.ts index d691f0b0a19..d4cf0ee4d24 100644 --- a/src/functions/events/v2.ts +++ b/src/functions/events/v2.ts @@ -18,9 +18,12 @@ export const DATABASE_EVENTS = [ export const REMOTE_CONFIG_EVENT = "google.firebase.remoteconfig.remoteConfig.v1.updated"; +export const TEST_LAB_EVENT = "google.firebase.testlab.testMatrix.v1.completed"; + export type Event = | typeof PUBSUB_PUBLISH_EVENT | typeof STORAGE_EVENTS[number] | typeof FIREBASE_ALERTS_PUBLISH_EVENT | typeof DATABASE_EVENTS[number] - | typeof REMOTE_CONFIG_EVENT; + | typeof REMOTE_CONFIG_EVENT + | typeof TEST_LAB_EVENT; diff --git a/src/test/deploy/functions/checkIam.spec.ts b/src/test/deploy/functions/checkIam.spec.ts index c130e75dffd..ac62d6ecaae 100644 --- a/src/test/deploy/functions/checkIam.spec.ts +++ b/src/test/deploy/functions/checkIam.spec.ts @@ -518,4 +518,91 @@ describe("checkIam", () => { expect(getIamStub).to.not.have.been.called; expect(setIamStub).to.not.have.been.called; }); + + it("should add the default bindings for a new v2 test lab function without v2 deployed functions", async () => { + const newIamPolicy = { + etag: "etag", + version: 3, + bindings: [ + BINDING, + { + role: checkIam.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE, + members: [ + `serviceAccount:service-${projectNumber}@gcp-sa-pubsub.iam.gserviceaccount.com`, + ], + }, + { + role: checkIam.RUN_INVOKER_ROLE, + members: [`serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`], + }, + { + role: checkIam.EVENTARC_EVENT_RECEIVER_ROLE, + members: [`serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`], + }, + ], + }; + getIamStub.resolves({ + etag: "etag", + version: 3, + bindings: [BINDING], + }); + setIamStub.resolves(newIamPolicy); + const wantFn: backend.Endpoint = { + id: "wantFn", + entryPoint: "wantFn", + platform: "gcfv2", + eventTrigger: { + eventType: "google.firebase.testlab.testMatrix.v1.completed", + eventFilters: {}, + retry: false, + }, + ...SPEC, + }; + + await checkIam.ensureServiceAgentRoles( + projectId, + projectNumber, + backend.of(wantFn), + backend.empty() + ); + + expect(getIamStub).to.have.been.calledOnce; + expect(setIamStub).to.have.been.calledOnce; + expect(setIamStub).to.have.been.calledWith(projectNumber, newIamPolicy, "bindings"); + }); + + it("should not add bindings for a new v2 test lab function with v2 deployed functions", async () => { + const wantFn: backend.Endpoint = { + id: "wantFn", + entryPoint: "wantFn", + platform: "gcfv2", + eventTrigger: { + eventType: "google.firebase.testlab.testMatrix.v1.completed", + eventFilters: {}, + retry: false, + }, + ...SPEC, + }; + const haveFn: backend.Endpoint = { + id: "haveFn", + entryPoint: "haveFn", + platform: "gcfv2", + eventTrigger: { + eventType: "google.cloud.storage.object.v1.finalized", + eventFilters: { bucket: "my-bucket" }, + retry: false, + }, + ...SPEC, + }; + + await checkIam.ensureServiceAgentRoles( + projectId, + projectNumber, + backend.of(wantFn), + backend.of(haveFn) + ); + + expect(getIamStub).to.not.have.been.called; + expect(setIamStub).to.not.have.been.called; + }); }); diff --git a/src/test/deploy/functions/services/testLab.spec.ts b/src/test/deploy/functions/services/testLab.spec.ts new file mode 100644 index 00000000000..84142b6cc20 --- /dev/null +++ b/src/test/deploy/functions/services/testLab.spec.ts @@ -0,0 +1,47 @@ +import { expect } from "chai"; +import { Endpoint } from "../../../../deploy/functions/backend"; +import * as testLab from "../../../../deploy/functions/services/testLab"; + +const projectNumber = "123456789"; + +const endpoint: Endpoint = { + id: "endpoint", + region: "us-central1", + project: projectNumber, + eventTrigger: { + retry: false, + eventType: "google.firebase.testlab.testMatrix.v1.completed", + eventFilters: {}, + }, + entryPoint: "endpoint", + platform: "gcfv2", + runtime: "nodejs16", +}; + +describe("ensureTestLabTriggerRegion", () => { + it("should set the trigger location to global", async () => { + const ep = { ...endpoint }; + + await testLab.ensureTestLabTriggerRegion(ep); + + expect(ep.eventTrigger.region).to.eq("global"); + }); + + it("should not error if the trigger location is global", async () => { + const ep = { ...endpoint }; + ep.eventTrigger.region = "global"; + + await testLab.ensureTestLabTriggerRegion(ep); + + expect(ep.eventTrigger.region).to.eq("global"); + }); + + it("should error if the trigger location is not global", () => { + const ep = { ...endpoint }; + ep.eventTrigger.region = "us-west1"; + + expect(() => testLab.ensureTestLabTriggerRegion(ep)).to.throw( + "A test lab trigger must specify 'global' trigger location" + ); + }); +});