-
Notifications
You must be signed in to change notification settings - Fork 961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for firealerts events in Eventarc emulator. #7355
Changes from 2 commits
8eff3d8
62b94e7
7b7d22f
1194f23
5f33271
046a959
35128d9
702002d
7c8e9bd
9b349a6
fabd95f
35b1eb3
b619e16
36a2ebe
b378930
9131cac
57efc86
736417b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
import { FirebaseError } from "../error"; | ||
import { cloudEventFromProtoToJson } from "./eventarcEmulatorUtils"; | ||
|
||
interface CustomEventTrigger { | ||
interface EmulatedEventTrigger { | ||
projectId: string; | ||
triggerName: string; | ||
eventTrigger: EventTrigger; | ||
|
@@ -29,7 +29,8 @@ | |
private destroyServer?: () => Promise<void>; | ||
|
||
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) {} | ||
|
||
|
@@ -46,32 +47,48 @@ | |
} | ||
const bodyString = (req as RequestWithRawBody).rawBody.toString(); | ||
const substituted = bodyString.replaceAll("${PROJECT_ID}", projectId); | ||
const body = JSON.parse(substituted); | ||
const eventTrigger = body.eventTrigger as EventTrigger; | ||
if (!eventTrigger) { | ||
const error = `Missing event trigger for ${triggerName}.`; | ||
this.logger.log("ERROR", error); | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again here - I don't a compelling reason why we need to track the custom and native events in separate dictionaries. |
||
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}`; | ||
const body = JSON.parse((req as RequestWithRawBody).rawBody.toString()); | ||
for (const event of body.events) { | ||
if (!event.type) { | ||
res.sendStatus(400); | ||
return; | ||
} | ||
|
@@ -79,7 +96,7 @@ | |
"INFO", | ||
`Received custom event at channel ${channel}: ${JSON.stringify(event, null, 2)}`, | ||
); | ||
this.triggerCustomEventFunction(channel, event); | ||
Check warning on line 99 in src/emulator/eventarcEmulator.ts GitHub Actions / lint (20)
|
||
} | ||
res.sendStatus(200); | ||
}; | ||
|
@@ -95,17 +112,33 @@ | |
}); | ||
}; | ||
|
||
const publishNativeEventsRoute = `/google/publishEvents`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here - the native events handler is essentially doing the same job as the custom events handler. |
||
const publishNativeEventsHandler: express.RequestHandler = (req, res) => { | ||
const body = JSON.parse((req as RequestWithRawBody).rawBody.toString()); | ||
for (const event of body.events as CloudEvent<any>[]) { | ||
Check warning on line 118 in src/emulator/eventarcEmulator.ts GitHub Actions / lint (20)
|
||
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); | ||
}); | ||
return hub; | ||
} | ||
|
||
async triggerCustomEventFunction(channel: string, event: CloudEvent<any>) { | ||
async triggerCustomEventFunction(channel: string, event: CloudEvent<any>): Promise<void[]> { | ||
if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { | ||
this.logger.log("INFO", "Functions emulator not found. This should not happen."); | ||
return Promise.reject(); | ||
|
@@ -119,31 +152,50 @@ | |
!trigger.eventTrigger.eventFilters || | ||
this.matchesAll(event, trigger.eventTrigger.eventFilters), | ||
) | ||
.map((trigger) => | ||
EmulatorRegistry.client(Emulators.FUNCTIONS) | ||
.request<CloudEvent<any>, 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<any>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm noticing that the |
||
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<any>): Promise<void> { | ||
return EmulatorRegistry.client(Emulators.FUNCTIONS) | ||
.request<CloudEvent<any>, NodeJS.ReadableStream>({ | ||
method: "POST", | ||
path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, | ||
body: JSON.stringify(event), | ||
responseType: "stream", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: why is the response type here stream? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not 100% of this either. I know it is showing up as a diff here, but that's just because I extracted out some of the previous code into a function. I am inclined to leave it this way as it is working fine with supporting the new events and I don't want to break anything that was working previously. |
||
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<any>, eventFilters: Record<string, string>): boolean { | ||
return Object.entries(eventFilters).every(([key, value]) => { | ||
let attr = event[key] ?? event.attributes[key]; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*nit - how about EventarcTrigger? I don't feel strongly about this but it may make sense to apply a more specific naming convention, in case we add other event trigger interfaces for other emulated services.