From 8eff3d8b08620f46becae053f620eb0c3803bc7b Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Fri, 21 Jun 2024 13:58:23 -0400 Subject: [PATCH 01/10] Allow functions emulator to recognize firealerts triggers --- src/emulator/constants.ts | 1 + src/emulator/functionsEmulator.ts | 7 +++++++ src/emulator/functionsEmulatorShared.ts | 4 ++++ src/functions/events/v2.ts | 6 +++++- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 5731ae2cb72..2c05f4756df 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -95,6 +95,7 @@ export class Constants { static SERVICE_REALTIME_DATABASE = "firebaseio.com"; static SERVICE_PUBSUB = "pubsub.googleapis.com"; static SERVICE_EVENTARC = "eventarc.googleapis.com"; + static SERVICE_FIREALERTS = "firebasealerts.googleapis.com"; // Note: the service name below are here solely for logging purposes. // There is not an emulator available for these. static SERVICE_ANALYTICS = "app-measurement.com"; diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index b29edf669cb..241d85ce51a 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -682,6 +682,13 @@ export class FunctionsEmulator implements EmulatorInstance { case Constants.SERVICE_STORAGE: added = this.addStorageTrigger(this.args.projectId, key, definition.eventTrigger); break; + case Constants.SERVICE_FIREALERTS: + added = await this.addEventarcTrigger( + this.args.projectId, + key, + definition.eventTrigger, + ); + break; default: this.logger.log("DEBUG", `Unsupported trigger: ${JSON.stringify(definition)}`); break; diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index 3ff9e1955c2..0cf32724b26 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -21,6 +21,7 @@ import { connectableHostname } from "../utils"; /** The current v2 events that are implemented in the emulator */ const V2_EVENTS = [ events.v2.PUBSUB_PUBLISH_EVENT, + events.v2.FIREALERTS_EVENT, ...events.v2.STORAGE_EVENTS, ...events.v2.DATABASE_EVENTS, ...events.v2.FIRESTORE_EVENTS, @@ -367,6 +368,9 @@ export function getServiceFromEventType(eventType: string): string { if (eventType.includes("storage")) { return Constants.SERVICE_STORAGE; } + if (eventType.includes("firebasealerts")) { + return Constants.SERVICE_FIREALERTS; + } // Below this point are services that do not have a emulator. if (eventType.includes("analytics")) { return Constants.SERVICE_ANALYTICS; diff --git a/src/functions/events/v2.ts b/src/functions/events/v2.ts index 84fbd72085f..15132856a93 100644 --- a/src/functions/events/v2.ts +++ b/src/functions/events/v2.ts @@ -30,6 +30,9 @@ export const FIRESTORE_EVENTS = [ "google.cloud.firestore.document.v1.updated.withAuthContext", "google.cloud.firestore.document.v1.deleted.withAuthContext", ] as const; + +export const FIREALERTS_EVENT = "google.firebase.firebasealerts.alerts.v1.published"; + export const FIRESTORE_EVENT_REGEX = /^google\.cloud\.firestore\.document\.v1\.[^\.]*$/; export const FIRESTORE_EVENT_WITH_AUTH_CONTEXT_REGEX = /^google\.cloud\.firestore\.document\.v1\..*\.withAuthContext$/; @@ -41,4 +44,5 @@ export type Event = | (typeof DATABASE_EVENTS)[number] | typeof REMOTE_CONFIG_EVENT | typeof TEST_LAB_EVENT - | (typeof FIRESTORE_EVENTS)[number]; + | (typeof FIRESTORE_EVENTS)[number] + | typeof FIREALERTS_EVENT; From 62b94e7bd876ae8313ad4f67252c16d48f65ac80 Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Fri, 21 Jun 2024 14:17:15 -0400 Subject: [PATCH 02/10] Add support for non custom events in event arc emulator --- src/emulator/eventarcEmulator.ts | 120 +++++++++++++++++++++--------- src/emulator/functionsEmulator.ts | 26 ++++++- 2 files changed, 111 insertions(+), 35 deletions(-) diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index 3c561a8f3c5..ad9ac16838d 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -10,7 +10,7 @@ import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; -interface CustomEventTrigger { +interface EmulatedEventTrigger { projectId: string; triggerName: string; eventTrigger: EventTrigger; @@ -29,7 +29,8 @@ export class EventarcEmulator implements EmulatorInstance { private destroyServer?: () => Promise; private logger = EmulatorLogger.forEmulator(Emulators.EVENTARC); - private customEvents: { [key: string]: CustomEventTrigger[] } = {}; + private customEvents: { [key: string]: EmulatedEventTrigger[] } = {}; + private nativeEvents: { [key: string]: EmulatedEventTrigger[] } = {}; constructor(private args: EventarcEmulatorArgs) {} @@ -54,18 +55,34 @@ export class EventarcEmulator implements EmulatorInstance { res.status(400).send({ error }); return; } - const key = `${eventTrigger.eventType}-${eventTrigger.channel}`; - this.logger.logLabeled( - "BULLET", - "eventarc", - `Registering custom event trigger for ${key} with trigger name ${triggerName}.`, - ); - const customEventTriggers = this.customEvents[key] || []; - customEventTriggers.push({ projectId, triggerName, eventTrigger }); - this.customEvents[key] = customEventTriggers; + if (eventTrigger.channel) { + const key = `${eventTrigger.eventType}-${eventTrigger.channel}`; + this.logger.logLabeled( + "BULLET", + "eventarc", + `Registering custom event trigger for ${key} with trigger name ${triggerName}.`, + ); + const customEventTriggers = this.customEvents[key] || []; + customEventTriggers.push({ projectId, triggerName, eventTrigger }); + this.customEvents[key] = customEventTriggers; + } else { + this.logger.logLabeled( + "BULLET", + "eventarc", + `Registering custom event trigger for ${eventTrigger.eventType} with trigger name ${triggerName}.`, + ); + const nativeEventTriggers = this.nativeEvents[eventTrigger.eventType] || []; + nativeEventTriggers.push({ projectId, triggerName, eventTrigger }); + this.nativeEvents[eventTrigger.eventType] = nativeEventTriggers; + } res.status(200).send({ res: "OK" }); }; + const getTriggersRoute = `/google/getTriggers`; + const getTriggersHandler: express.RequestHandler = (req, res) => { + res.status(200).send(this.nativeEvents); + }; + const publishEventsRoute = `/projects/:project_id/locations/:location/channels/:channel::publishEvents`; const publishEventsHandler: express.RequestHandler = (req, res) => { const channel = `projects/${req.params.project_id}/locations/${req.params.location}/channels/${req.params.channel}`; @@ -95,9 +112,25 @@ export class EventarcEmulator implements EmulatorInstance { }); }; + const publishNativeEventsRoute = `/google/publishEvents`; + const publishNativeEventsHandler: express.RequestHandler = (req, res) => { + const body = JSON.parse((req as RequestWithRawBody).rawBody.toString()); + for (const event of body.events as CloudEvent[]) { + if (!event.type) { + res.sendStatus(400); + return; + } + this.logger.log("INFO", `Received ${event.type} event: ${JSON.stringify(event, null, 2)}`); + void this.triggerNativeEventFunction(event); + } + res.sendStatus(200); + }; + const hub = express(); hub.post([registerTriggerRoute], dataMiddleware, registerTriggerHandler); hub.post([publishEventsRoute], dataMiddleware, publishEventsHandler); + hub.post([publishNativeEventsRoute], dataMiddleware, publishNativeEventsHandler); + hub.get([getTriggersRoute], dataMiddleware, getTriggersHandler); hub.all("*", (req, res) => { this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); res.sendStatus(404); @@ -105,7 +138,7 @@ export class EventarcEmulator implements EmulatorInstance { return hub; } - async triggerCustomEventFunction(channel: string, event: CloudEvent) { + async triggerCustomEventFunction(channel: string, event: CloudEvent): Promise { if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { this.logger.log("INFO", "Functions emulator not found. This should not happen."); return Promise.reject(); @@ -119,31 +152,50 @@ export class EventarcEmulator implements EmulatorInstance { !trigger.eventTrigger.eventFilters || this.matchesAll(event, trigger.eventTrigger.eventFilters), ) - .map((trigger) => - EmulatorRegistry.client(Emulators.FUNCTIONS) - .request, NodeJS.ReadableStream>({ - method: "POST", - path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, - body: JSON.stringify(cloudEventFromProtoToJson(event)), - responseType: "stream", - resolveOnHTTPError: true, - }) - .then((res) => { - // Since the response type is a stream and using `resolveOnHTTPError: true`, we check status manually. - if (res.status >= 400) { - throw new FirebaseError(`Received non-200 status code: ${res.status}`); - } - }) - .catch((err) => { - this.logger.log( - "ERROR", - `Failed to trigger Functions emulator for ${trigger.triggerName}: ${err}`, - ); - }), - ), + .map((trigger) => this.callFunctionTrigger(trigger, cloudEventFromProtoToJson(event))), + ); + } + + async triggerNativeEventFunction(event: CloudEvent) { + if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + this.logger.log("INFO", "Functions emulator not found. This should not happen."); + return Promise.reject(); + } + const triggers = this.nativeEvents[event.type] || []; + return await Promise.all( + triggers + .filter( + (trigger) => + !trigger.eventTrigger.eventFilters || + this.matchesAll(event, trigger.eventTrigger.eventFilters), + ) + .map((trigger) => this.callFunctionTrigger(trigger, event)), ); } + callFunctionTrigger(trigger: EmulatedEventTrigger, event: CloudEvent): Promise { + return EmulatorRegistry.client(Emulators.FUNCTIONS) + .request, NodeJS.ReadableStream>({ + method: "POST", + path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, + body: JSON.stringify(event), + responseType: "stream", + resolveOnHTTPError: true, + }) + .then((res) => { + // Since the response type is a stream and using `resolveOnHTTPError: true`, we check status manually. + if (res.status >= 400) { + throw new FirebaseError(`Received non-200 status code: ${res.status}`); + } + }) + .catch((err) => { + this.logger.log( + "ERROR", + `Failed to trigger Functions emulator for ${trigger.triggerName}: ${err}`, + ); + }); + } + private matchesAll(event: CloudEvent, eventFilters: Record): boolean { return Object.entries(eventFilters).every(([key, value]) => { let attr = event[key] ?? event.attributes[key]; diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 241d85ce51a..3597573009e 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -683,7 +683,7 @@ export class FunctionsEmulator implements EmulatorInstance { added = this.addStorageTrigger(this.args.projectId, key, definition.eventTrigger); break; case Constants.SERVICE_FIREALERTS: - added = await this.addEventarcTrigger( + added = await this.addFirealertsTrigger( this.args.projectId, key, definition.eventTrigger, @@ -775,6 +775,30 @@ export class FunctionsEmulator implements EmulatorInstance { }); } + addFirealertsTrigger( + projectId: string, + key: string, + eventTrigger: EventTrigger, + ): Promise { + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { + return Promise.resolve(false); + } + const bundle = { + eventTrigger: { + ...eventTrigger, + service: "firebasealerts.googleapis.com", + }, + }; + logger.debug(`addFirealertsTrigger`, JSON.stringify(bundle)); + return EmulatorRegistry.client(Emulators.EVENTARC) + .post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle) + .then(() => true) + .catch((err) => { + this.logger.log("WARN", "Error adding FireAlerts function: " + err); + return false; + }); + } + async performPostLoadOperations(): Promise { if ( !this.blockingFunctionsConfig.triggers && From 7b7d22f5cbc28a80e1c214b24a0035015437d11b Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Mon, 24 Jun 2024 13:50:13 -0400 Subject: [PATCH 03/10] add cors to getTriggers route --- src/emulator/eventarcEmulator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index ad9ac16838d..5e203eef297 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -9,6 +9,7 @@ import { CloudEvent } from "./events/types"; import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; +import * as cors from "cors"; interface EmulatedEventTrigger { projectId: string; @@ -130,7 +131,7 @@ export class EventarcEmulator implements EmulatorInstance { hub.post([registerTriggerRoute], dataMiddleware, registerTriggerHandler); hub.post([publishEventsRoute], dataMiddleware, publishEventsHandler); hub.post([publishNativeEventsRoute], dataMiddleware, publishNativeEventsHandler); - hub.get([getTriggersRoute], dataMiddleware, getTriggersHandler); + hub.get([getTriggersRoute], cors({ origin: true }), getTriggersHandler); hub.all("*", (req, res) => { this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); res.sendStatus(404); From 1194f230e0e872772c454333c3058e3eb7d4680b Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Tue, 25 Jun 2024 10:07:22 -0400 Subject: [PATCH 04/10] Remove duplicate logic --- src/emulator/eventarcEmulator.ts | 89 ++++++++++---------------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index 5e203eef297..f37b1452e5e 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -25,13 +25,13 @@ export interface EventarcEmulatorArgs { port?: number; host?: string; } +const GOOGLE_CHANNEL = "google"; export class EventarcEmulator implements EmulatorInstance { private destroyServer?: () => Promise; private logger = EmulatorLogger.forEmulator(Emulators.EVENTARC); - private customEvents: { [key: string]: EmulatedEventTrigger[] } = {}; - private nativeEvents: { [key: string]: EmulatedEventTrigger[] } = {}; + private events: { [key: string]: EmulatedEventTrigger[] } = {}; constructor(private args: EventarcEmulatorArgs) {} @@ -56,37 +56,34 @@ export class EventarcEmulator implements EmulatorInstance { res.status(400).send({ error }); return; } - if (eventTrigger.channel) { - const key = `${eventTrigger.eventType}-${eventTrigger.channel}`; - this.logger.logLabeled( - "BULLET", - "eventarc", - `Registering custom event trigger for ${key} with trigger name ${triggerName}.`, - ); - const customEventTriggers = this.customEvents[key] || []; - customEventTriggers.push({ projectId, triggerName, eventTrigger }); - this.customEvents[key] = customEventTriggers; - } else { - this.logger.logLabeled( - "BULLET", - "eventarc", - `Registering custom event trigger for ${eventTrigger.eventType} with trigger name ${triggerName}.`, - ); - const nativeEventTriggers = this.nativeEvents[eventTrigger.eventType] || []; - nativeEventTriggers.push({ projectId, triggerName, eventTrigger }); - this.nativeEvents[eventTrigger.eventType] = nativeEventTriggers; - } + const channel = eventTrigger.channel || GOOGLE_CHANNEL; + const key = `${eventTrigger.eventType}-${channel}`; + this.logger.logLabeled( + "BULLET", + "eventarc", + `Registering Eventarc event trigger for ${key} with trigger name ${triggerName}.`, + ); + const eventTriggers = this.events[key] || []; + eventTriggers.push({ projectId, triggerName, eventTrigger }); + this.events[key] = eventTriggers; res.status(200).send({ res: "OK" }); }; const getTriggersRoute = `/google/getTriggers`; const getTriggersHandler: express.RequestHandler = (req, res) => { - res.status(200).send(this.nativeEvents); + res.status(200).send(this.events); }; const publishEventsRoute = `/projects/:project_id/locations/:location/channels/:channel::publishEvents`; + const publishNativeEventsRoute = `/google/publishEvents`; + const publishEventsHandler: express.RequestHandler = (req, res) => { - const channel = `projects/${req.params.project_id}/locations/${req.params.location}/channels/${req.params.channel}`; + const isCustom = req.params.project_id && req.params.channel; + + const channel = isCustom + ? `projects/${req.params.project_id}/locations/${req.params.location}/channels/${req.params.channel}` + : GOOGLE_CHANNEL; + const body = JSON.parse((req as RequestWithRawBody).rawBody.toString()); for (const event of body.events) { if (!event.type) { @@ -95,9 +92,9 @@ export class EventarcEmulator implements EmulatorInstance { } this.logger.log( "INFO", - `Received custom event at channel ${channel}: ${JSON.stringify(event, null, 2)}`, + `Received event at channel ${channel}: ${JSON.stringify(event, null, 2)}`, ); - this.triggerCustomEventFunction(channel, event); + this.triggerEventFunction(channel, event); } res.sendStatus(200); }; @@ -113,24 +110,10 @@ export class EventarcEmulator implements EmulatorInstance { }); }; - const publishNativeEventsRoute = `/google/publishEvents`; - const publishNativeEventsHandler: express.RequestHandler = (req, res) => { - const body = JSON.parse((req as RequestWithRawBody).rawBody.toString()); - for (const event of body.events as CloudEvent[]) { - if (!event.type) { - res.sendStatus(400); - return; - } - this.logger.log("INFO", `Received ${event.type} event: ${JSON.stringify(event, null, 2)}`); - void this.triggerNativeEventFunction(event); - } - res.sendStatus(200); - }; - const hub = express(); hub.post([registerTriggerRoute], dataMiddleware, registerTriggerHandler); hub.post([publishEventsRoute], dataMiddleware, publishEventsHandler); - hub.post([publishNativeEventsRoute], dataMiddleware, publishNativeEventsHandler); + hub.post([publishNativeEventsRoute], dataMiddleware, publishEventsHandler); hub.get([getTriggersRoute], cors({ origin: true }), getTriggersHandler); hub.all("*", (req, res) => { this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); @@ -139,30 +122,14 @@ export class EventarcEmulator implements EmulatorInstance { return hub; } - async triggerCustomEventFunction(channel: string, event: CloudEvent): Promise { + async triggerEventFunction(channel: string, event: CloudEvent): Promise { if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { this.logger.log("INFO", "Functions emulator not found. This should not happen."); return Promise.reject(); } const key = `${event.type}-${channel}`; - const triggers = this.customEvents[key] || []; - return await Promise.all( - triggers - .filter( - (trigger) => - !trigger.eventTrigger.eventFilters || - this.matchesAll(event, trigger.eventTrigger.eventFilters), - ) - .map((trigger) => this.callFunctionTrigger(trigger, cloudEventFromProtoToJson(event))), - ); - } - - async triggerNativeEventFunction(event: CloudEvent) { - if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { - this.logger.log("INFO", "Functions emulator not found. This should not happen."); - return Promise.reject(); - } - const triggers = this.nativeEvents[event.type] || []; + const triggers = this.events[key] || []; + const eventPayload = channel === GOOGLE_CHANNEL ? event : cloudEventFromProtoToJson(event); return await Promise.all( triggers .filter( @@ -170,7 +137,7 @@ export class EventarcEmulator implements EmulatorInstance { !trigger.eventTrigger.eventFilters || this.matchesAll(event, trigger.eventTrigger.eventFilters), ) - .map((trigger) => this.callFunctionTrigger(trigger, event)), + .map((trigger) => this.callFunctionTrigger(trigger, eventPayload)), ); } From 5f332719c926b00523f1590a4a81b42b5b3bea9c Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Thu, 27 Jun 2024 11:38:19 -0400 Subject: [PATCH 05/10] add cors to publish native events route --- src/emulator/eventarcEmulator.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index f37b1452e5e..35b41e5948d 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -113,7 +113,12 @@ export class EventarcEmulator implements EmulatorInstance { const hub = express(); hub.post([registerTriggerRoute], dataMiddleware, registerTriggerHandler); hub.post([publishEventsRoute], dataMiddleware, publishEventsHandler); - hub.post([publishNativeEventsRoute], dataMiddleware, publishEventsHandler); + hub.post( + [publishNativeEventsRoute], + dataMiddleware, + cors({ origin: true }), + publishEventsHandler, + ); hub.get([getTriggersRoute], cors({ origin: true }), getTriggersHandler); hub.all("*", (req, res) => { this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); From 702002d2cd5238d592fbce57228f88cf2025e983 Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Tue, 9 Jul 2024 10:45:11 -0400 Subject: [PATCH 06/10] Add ability to remove triggers --- src/emulator/eventarcEmulator.ts | 66 +++++++++++++++++----- src/emulator/functionsEmulator.ts | 93 +++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 14 deletions(-) diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index 35b41e5948d..33027e3668e 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -33,18 +33,34 @@ export class EventarcEmulator implements EmulatorInstance { private logger = EmulatorLogger.forEmulator(Emulators.EVENTARC); private events: { [key: string]: EmulatedEventTrigger[] } = {}; - constructor(private args: EventarcEmulatorArgs) {} + constructor(private args: EventarcEmulatorArgs) { } createHubServer(): express.Application { const registerTriggerRoute = `/emulator/v1/projects/:project_id/triggers/:trigger_name(*)`; const registerTriggerHandler: express.RequestHandler = (req, res) => { + try { + const { projectId, triggerName, eventTrigger, key } = getTriggerIdentifiers(req); + this.logger.logLabeled( + "BULLET", + "eventarc", + `Registering Eventarc event trigger for ${key} with trigger name ${triggerName}.`, + ); + const eventTriggers = this.events[key] || []; + eventTriggers.push({ projectId, triggerName, eventTrigger }); + this.events[key] = eventTriggers; + res.status(200).send({ res: "OK" }); + } catch (error) { + res.status(400).send({ error }); + } + }; + + const getTriggerIdentifiers = (req: express.Request) => { const projectId = req.params.project_id; const triggerName = req.params.trigger_name; if (!projectId || !triggerName) { const error = "Missing project ID or trigger name."; this.logger.log("ERROR", error); - res.status(400).send({ error }); - return; + throw error; } const bodyString = (req as RequestWithRawBody).rawBody.toString(); const substituted = bodyString.replaceAll("${PROJECT_ID}", projectId); @@ -53,20 +69,41 @@ export class EventarcEmulator implements EmulatorInstance { if (!eventTrigger) { const error = `Missing event trigger for ${triggerName}.`; this.logger.log("ERROR", error); - res.status(400).send({ error }); - return; + throw error; } const channel = eventTrigger.channel || GOOGLE_CHANNEL; const key = `${eventTrigger.eventType}-${channel}`; - this.logger.logLabeled( - "BULLET", - "eventarc", - `Registering Eventarc event trigger for ${key} with trigger name ${triggerName}.`, - ); - const eventTriggers = this.events[key] || []; - eventTriggers.push({ projectId, triggerName, eventTrigger }); - this.events[key] = eventTriggers; - res.status(200).send({ res: "OK" }); + return { projectId, triggerName, eventTrigger, key }; + }; + + const removeTriggerRoute = `/emulator/v1/remove/projects/:project_id/triggers/:trigger_name`; + const removeTriggerHandler: express.RequestHandler = (req, res) => { + try { + const { projectId, triggerName, eventTrigger, key } = getTriggerIdentifiers(req); + this.logger.logLabeled( + "BULLET", + "eventarc", + `Removing Eventarc event trigger for ${key} with trigger name ${triggerName}.`, + ); + const eventTriggers = this.events[key] || []; + const triggerIdentifier = { projectId, triggerName, eventTrigger }; + const removeIdx = eventTriggers.findIndex( + (e) => JSON.stringify(triggerIdentifier) === JSON.stringify(e), + ); + if (removeIdx === -1) { + this.logger.logLabeled("ERROR", "eventarc", "Tried to remove nonexistent trigger"); + throw new Error(`Unable to delete function trigger ${triggerName}`); + } + eventTriggers.splice(removeIdx, 1); + if (eventTriggers.length === 0) { + delete this.events[key]; + } else { + this.events[key] = eventTriggers; + } + res.status(200).send({ res: "OK" }); + } catch (error) { + res.status(400).send({ error }); + } }; const getTriggersRoute = `/google/getTriggers`; @@ -119,6 +156,7 @@ export class EventarcEmulator implements EmulatorInstance { cors({ origin: true }), publishEventsHandler, ); + hub.post([removeTriggerRoute], dataMiddleware, removeTriggerHandler); hub.get([getTriggersRoute], cors({ origin: true }), getTriggersHandler); hub.all("*", (req, res) => { this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 3597573009e..558a224c72e 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -587,6 +587,24 @@ export class FunctionsEmulator implements EmulatorInstance { this.workerPools[emulatableBackend.codebase].refresh(); } + // Remove any old trigger definitions + const keysToRemove = Object.keys(this.triggers).filter((recordKey) => { + const record = this.getTriggerRecordByKey(recordKey); + if (force) { + return true; // We are going to load all of the triggers anyway, so we can remove everything + } + return !triggerDefinitions.some( + (def) => + record.def.entryPoint === def.entryPoint && + JSON.stringify(record.def.eventTrigger) === JSON.stringify(def.eventTrigger), + ); + }); + const toRemove = keysToRemove.map((key) => this.getTriggerRecordByKey(key).def); + keysToRemove.forEach((key) => { + delete this.triggers[key]; + }); + await this.removeTriggers(toRemove); + // When force is true we set up all triggers, otherwise we only set up // triggers which have a unique function name const toSetup = triggerDefinitions.filter((definition) => { @@ -755,6 +773,33 @@ export class FunctionsEmulator implements EmulatorInstance { } } + // Currently only cleans up eventarc and firealerts triggers + async removeTriggers(toRemove: EmulatedTriggerDefinition[]) { + for (const definition of toRemove) { + const service = getFunctionService(definition); + const key = this.getTriggerKey(definition); + + switch (service) { + case Constants.SERVICE_EVENTARC: + await this.removeEventarcTrigger( + this.args.projectId, + key, + definition.eventTrigger as EventTrigger, + ); + break; + case Constants.SERVICE_FIREALERTS: + await this.removeFirealertsTrigger( + this.args.projectId, + key, + definition.eventTrigger as EventTrigger, + ); + break; + default: + break; + } + } + } + addEventarcTrigger(projectId: string, key: string, eventTrigger: EventTrigger): Promise { if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { return Promise.resolve(false); @@ -775,6 +820,30 @@ export class FunctionsEmulator implements EmulatorInstance { }); } + removeEventarcTrigger( + projectId: string, + key: string, + eventTrigger: EventTrigger, + ): Promise { + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { + return Promise.resolve(false); + } + const bundle = { + eventTrigger: { + ...eventTrigger, + service: "eventarc.googleapis.com", + }, + }; + logger.debug(`removeEventarcTrigger`, JSON.stringify(bundle)); + return EmulatorRegistry.client(Emulators.EVENTARC) + .post(`/emulator/v1/remove/projects/${projectId}/triggers/${key}`, bundle) + .then(() => true) + .catch((err) => { + this.logger.log("WARN", "Error removing Eventarc function: " + err); + return false; + }); + } + addFirealertsTrigger( projectId: string, key: string, @@ -799,6 +868,30 @@ export class FunctionsEmulator implements EmulatorInstance { }); } + removeFirealertsTrigger( + projectId: string, + key: string, + eventTrigger: EventTrigger, + ): Promise { + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { + return Promise.resolve(false); + } + const bundle = { + eventTrigger: { + ...eventTrigger, + service: "firebasealerts.googleapis.com", + }, + }; + logger.debug(`removeFirealertsTrigger`, JSON.stringify(bundle)); + return EmulatorRegistry.client(Emulators.EVENTARC) + .post(`/emulator/v1/remove/projects/${projectId}/triggers/${key}`, bundle) + .then(() => true) + .catch((err) => { + this.logger.log("WARN", "Error removing FireAlerts function: " + err); + return false; + }); + } + async performPostLoadOperations(): Promise { if ( !this.blockingFunctionsConfig.triggers && From 7c8e9bdc5b8c0996c721e2e8fadeada4288c7bea Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Tue, 9 Jul 2024 10:45:54 -0400 Subject: [PATCH 07/10] Fix prettier problem --- src/emulator/eventarcEmulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts index 33027e3668e..18e42814d44 100644 --- a/src/emulator/eventarcEmulator.ts +++ b/src/emulator/eventarcEmulator.ts @@ -33,7 +33,7 @@ export class EventarcEmulator implements EmulatorInstance { private logger = EmulatorLogger.forEmulator(Emulators.EVENTARC); private events: { [key: string]: EmulatedEventTrigger[] } = {}; - constructor(private args: EventarcEmulatorArgs) { } + constructor(private args: EventarcEmulatorArgs) {} createHubServer(): express.Application { const registerTriggerRoute = `/emulator/v1/projects/:project_id/triggers/:trigger_name(*)`; From 9b349a6a9456e8aec4e395ff790faeeb04e912b5 Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Tue, 9 Jul 2024 11:44:44 -0400 Subject: [PATCH 08/10] Only remove triggers with removal implemented --- src/emulator/functionsEmulator.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 558a224c72e..daa38606a49 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -588,7 +588,7 @@ export class FunctionsEmulator implements EmulatorInstance { } // Remove any old trigger definitions - const keysToRemove = Object.keys(this.triggers).filter((recordKey) => { + const toRemove = Object.keys(this.triggers).filter((recordKey) => { const record = this.getTriggerRecordByKey(recordKey); if (force) { return true; // We are going to load all of the triggers anyway, so we can remove everything @@ -599,10 +599,6 @@ export class FunctionsEmulator implements EmulatorInstance { JSON.stringify(record.def.eventTrigger) === JSON.stringify(def.eventTrigger), ); }); - const toRemove = keysToRemove.map((key) => this.getTriggerRecordByKey(key).def); - keysToRemove.forEach((key) => { - delete this.triggers[key]; - }); await this.removeTriggers(toRemove); // When force is true we set up all triggers, otherwise we only set up @@ -774,8 +770,9 @@ export class FunctionsEmulator implements EmulatorInstance { } // Currently only cleans up eventarc and firealerts triggers - async removeTriggers(toRemove: EmulatedTriggerDefinition[]) { - for (const definition of toRemove) { + async removeTriggers(toRemove: string[]) { + for (const triggerKey of toRemove) { + const definition = this.triggers[triggerKey].def; const service = getFunctionService(definition); const key = this.getTriggerKey(definition); @@ -786,6 +783,7 @@ export class FunctionsEmulator implements EmulatorInstance { key, definition.eventTrigger as EventTrigger, ); + delete this.triggers[key]; break; case Constants.SERVICE_FIREALERTS: await this.removeFirealertsTrigger( @@ -793,6 +791,7 @@ export class FunctionsEmulator implements EmulatorInstance { key, definition.eventTrigger as EventTrigger, ); + delete this.triggers[key]; break; default: break; From b37893030020e36a4e5d93bd9d77c222410efb87 Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Mon, 22 Jul 2024 13:26:09 -0400 Subject: [PATCH 09/10] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c394f8ad5b2..ee0de086486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,4 @@ - GitHub Action fixes for web frameworks (#6883) - Fixes issue where PubSub message `publishTime` is set to 1970-01-01T00:00:00 (#7441) - Display meaningful error message when cannot determine target. (#6594) +- Adds support for firealerts events in Eventarc emulator. (#7355) From 57efc86bdaa3def5495bd147067be4ee23848aee Mon Sep 17 00:00:00 2001 From: Garrett Burroughs Date: Mon, 22 Jul 2024 13:36:21 -0400 Subject: [PATCH 10/10] update package-lock --- firebase-vscode/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-vscode/package-lock.json b/firebase-vscode/package-lock.json index aac177ef113..e1d605e32fb 100644 --- a/firebase-vscode/package-lock.json +++ b/firebase-vscode/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "firebase-vscode", - "version": "0.4.3", + "version": "0.4.4", "dependencies": { "@preact/signals-core": "^1.4.0", "@preact/signals-react": "1.3.6",