diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index dd18394bbcd..026411cdad6 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -37,6 +37,7 @@ functionsEmulator.nodeBinary = process.execPath; functionsEmulator.setTriggersForTesting([ { + platform: "gcfv1", name: "function_id", id: "us-central1-function_id", region: "us-central1", @@ -45,6 +46,7 @@ functionsEmulator.setTriggersForTesting([ labels: {}, }, { + platform: "gcfv1", name: "function_id", id: "europe-west2-function_id", region: "europe-west2", @@ -53,6 +55,7 @@ functionsEmulator.setTriggersForTesting([ labels: {}, }, { + platform: "gcfv1", name: "function_id", id: "europe-west3-function_id", region: "europe-west3", @@ -61,6 +64,7 @@ functionsEmulator.setTriggersForTesting([ labels: {}, }, { + platform: "gcfv1", name: "callable_function_id", id: "us-central1-callable_function_id", region: "us-central1", @@ -71,6 +75,7 @@ functionsEmulator.setTriggersForTesting([ }, }, { + platform: "gcfv1", name: "nested-function_id", id: "us-central1-nested-function_id", region: "us-central1", diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index ec5fe17941d..4d7ca8cb502 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -55,6 +55,7 @@ import { } from "./adminSdkConfig"; import * as functionsEnv from "../functions/env"; import { EventUtils } from "./events/types"; +import { functionIdsAreValid } from "../deploy/functions/validate"; const EVENT_INVOKE = "functions:invoke"; @@ -496,6 +497,18 @@ export class FunctionsEmulator implements EmulatorInstance { }); for (const definition of toSetup) { + // Skip function with invalid id. + try { + functionIdsAreValid([definition]); + } catch (e) { + this.logger.logLabeled( + "WARN", + `functions[${definition.id}]`, + `Invalid function id: ${e.message}` + ); + continue; + } + let added = false; let url: string | undefined = undefined; diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index fc5b9340d90..c41c568e5a4 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -13,6 +13,7 @@ export type SignatureType = "http" | "event" | "cloudevent"; export interface ParsedTriggerDefinition { entryPoint: string; + platform: FunctionsPlatform; name: string; timeout?: string | number; // Can be "3s" for some reason lol regions?: string[]; @@ -21,7 +22,6 @@ export interface ParsedTriggerDefinition { eventTrigger?: EventTrigger; schedule?: EventSchedule; labels?: { [key: string]: any }; - platform?: FunctionsPlatform; } export interface EmulatedTriggerDefinition extends ParsedTriggerDefinition { @@ -156,6 +156,7 @@ export function emulatedFunctionsByRegion( defDeepCopy.regions = [region]; defDeepCopy.region = region; defDeepCopy.id = `${region}-${defDeepCopy.name}`; + defDeepCopy.platform = defDeepCopy.platform || "gcfv1"; regionDefinitions.push(defDeepCopy); } diff --git a/src/emulator/pubsubEmulator.ts b/src/emulator/pubsubEmulator.ts index a6bd1e020fa..5d86aaa30c1 100644 --- a/src/emulator/pubsubEmulator.ts +++ b/src/emulator/pubsubEmulator.ts @@ -2,14 +2,15 @@ import * as uuid from "uuid"; import { MessagePublishedData } from "@google/events/cloud/pubsub/v1/MessagePublishedData"; import { Message, PubSub, Subscription } from "@google-cloud/pubsub"; -import * as api from "../api"; import * as downloadableEmulators from "./downloadableEmulators"; +import { Client } from "../apiv2"; import { EmulatorLogger } from "./emulatorLogger"; import { EmulatorInfo, EmulatorInstance, Emulators } from "../emulator/types"; import { Constants } from "./constants"; import { FirebaseError } from "../error"; import { EmulatorRegistry } from "./registry"; import { SignatureType } from "./functionsEmulatorShared"; +import { CloudEvent } from "./events/types"; export interface PubsubEmulatorArgs { projectId: string; @@ -32,6 +33,9 @@ export class PubsubEmulator implements EmulatorInstance { // Map of topic name to a PubSub subscription object subscriptionForTopic: Map; + // Client for communicating with the Functions Emulator + private client?: Client; + private logger = EmulatorLogger.forEmulator(Emulators.PUBSUB); constructor(private args: PubsubEmulatorArgs) { @@ -40,7 +44,6 @@ export class PubsubEmulator implements EmulatorInstance { apiEndpoint: `${host}:${port}`, projectId: this.args.projectId, }); - this.triggersForTopic = new Map(); this.subscriptionForTopic = new Map(); } @@ -124,59 +127,61 @@ export class PubsubEmulator implements EmulatorInstance { this.subscriptionForTopic.set(topicName, sub); } - private getRequestOptions( + private ensureFunctionsClient() { + if (this.client != undefined) return; + + const funcEmulator = EmulatorRegistry.get(Emulators.FUNCTIONS); + if (!funcEmulator) { + throw new FirebaseError( + `Attempted to execute pubsub trigger but could not find the Functions emulator` + ); + } + this.client = new Client({ + urlPrefix: `http://${EmulatorRegistry.getInfoHostString(funcEmulator.getInfo())}`, + auth: false, + }); + } + + private createLegacyEventRequestBody(topic: string, message: Message) { + return { + context: { + eventId: uuid.v4(), + resource: { + service: "pubsub.googleapis.com", + name: `projects/${this.args.projectId}/topics/${topic}`, + }, + eventType: "google.pubsub.topic.publish", + timestamp: message.publishTime.toISOString(), + }, + data: { + data: message.data, + attributes: message.attributes, + }, + }; + } + + private createCloudEventRequestBody( topic: string, - message: Message, - signatureType: SignatureType - ): Record { - const baseOpts = { - origin: `http://${EmulatorRegistry.getInfoHostString( - EmulatorRegistry.get(Emulators.FUNCTIONS)!.getInfo() - )}`, + message: Message + ): CloudEvent { + const data: MessagePublishedData = { + message: { + messageId: message.id, + publishTime: message.publishTime, + attributes: message.attributes, + orderingKey: message.orderingKey, + data: message.data.toString("base64"), + }, + subscription: this.subscriptionForTopic.get(topic)!.name, + }; + return { + specversion: "1", + id: uuid.v4(), + time: message.publishTime.toISOString(), + type: "google.cloud.pubsub.topic.v1.messagePublished", + source: `//pubsub.googleapis.com/projects/${this.args.projectId}/topics/${topic}`, + data, }; - if (signatureType === "event") { - return { - ...baseOpts, - data: { - context: { - eventId: uuid.v4(), - resource: { - service: "pubsub.googleapis.com", - name: `projects/${this.args.projectId}/topics/${topic}`, - }, - eventType: "google.pubsub.topic.publish", - timestamp: message.publishTime.toISOString(), - }, - data: { - data: message.data, - attributes: message.attributes, - }, - }, - }; - } else if (signatureType === "cloudevent") { - const data: MessagePublishedData = { - message: { - messageId: message.id, - publishTime: message.publishTime, - attributes: message.attributes, - orderingKey: message.orderingKey, - data: message.data.toString("base64"), - }, - subscription: this.subscriptionForTopic.get(topic)!.name, - }; - const ce = { - specVersion: 1, - type: "google.cloud.pubsub.topic.v1.messagePublished", - source: `//pubsub.googleapis.com/projects/${this.args.projectId}/topics/${topic}`, - data, - }; - return { - ...baseOpts, - headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" }, - data: ce, - }; - } - throw new FirebaseError(`Unsupported trigger signature: ${signatureType}`); } private async onMessage(topicName: string, message: Message) { @@ -186,12 +191,6 @@ export class PubsubEmulator implements EmulatorInstance { throw new FirebaseError(`No trigger for topic: ${topicName}`); } - if (!EmulatorRegistry.get(Emulators.FUNCTIONS)) { - throw new FirebaseError( - `Attempted to execute pubsub trigger for topic ${topicName} but could not find Functions emulator` - ); - } - this.logger.logLabeled( "DEBUG", "pubsub", @@ -200,14 +199,22 @@ export class PubsubEmulator implements EmulatorInstance { )})` ); + this.ensureFunctionsClient(); + for (const { triggerKey, signatureType } of triggers) { - const reqOpts = this.getRequestOptions(topicName, message, signatureType); try { - await api.request( - "POST", - `/functions/projects/${this.args.projectId}/triggers/${triggerKey}`, - reqOpts - ); + const path = `/functions/projects/${this.args.projectId}/triggers/${triggerKey}`; + if (signatureType === "event") { + await this.client!.post(path, this.createLegacyEventRequestBody(topicName, message)); + } else if (signatureType === "cloudevent") { + await this.client!.post, unknown>( + path, + this.createCloudEventRequestBody(topicName, message), + { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" } } + ); + } else { + throw new FirebaseError(`Unsupported trigger signature: ${signatureType}`); + } } catch (e) { this.logger.logLabeled("DEBUG", "pubsub", e); } diff --git a/src/emulator/storage/cloudFunctions.ts b/src/emulator/storage/cloudFunctions.ts index 071840362f7..8c914b0e9ea 100644 --- a/src/emulator/storage/cloudFunctions.ts +++ b/src/emulator/storage/cloudFunctions.ts @@ -1,9 +1,12 @@ +import * as uuid from "uuid"; + import { EmulatorRegistry } from "../registry"; import { EmulatorInfo, Emulators } from "../types"; import { EmulatorLogger } from "../emulatorLogger"; import { CloudStorageObjectMetadata, toSerializedDate } from "./metadata"; import { Client } from "../../apiv2"; import { StorageObjectData } from "@google/events/cloud/storage/v1/StorageObjectData"; +import { CloudEvent, LegacyEvent } from "../events/types"; type StorageCloudFunctionAction = "finalize" | "metadataUpdate" | "delete" | "archive"; const STORAGE_V2_ACTION_MAP: Record = { @@ -54,9 +57,13 @@ export class StorageCloudFunctions { } /** Modern CloudEvents */ const cloudEventBody = this.createCloudEventRequestBody(action, object); - const cloudEventRes = await this.client!.post(this.multicastPath, cloudEventBody, { - headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" }, - }); + const cloudEventRes = await this.client!.post, any>( + this.multicastPath, + cloudEventBody, + { + headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" }, + } + ); if (cloudEventRes.status !== 200) { errStatus.push(cloudEventRes.status); } @@ -77,9 +84,9 @@ export class StorageCloudFunctions { private createLegacyEventRequestBody( action: StorageCloudFunctionAction, objectMetadataPayload: ObjectMetadataPayload - ): string { + ) { const timestamp = new Date(); - return JSON.stringify({ + return { eventId: `${timestamp.getTime()}`, timestamp: toSerializedDate(timestamp), eventType: `google.storage.object.${action}`, @@ -89,25 +96,31 @@ export class StorageCloudFunctions { type: "storage#object", }, // bucket data: objectMetadataPayload, - }); + }; } /** Modern CloudEvents type */ private createCloudEventRequestBody( action: StorageCloudFunctionAction, objectMetadataPayload: ObjectMetadataPayload - ): string { + ): CloudEvent { const ceAction = STORAGE_V2_ACTION_MAP[action]; if (!ceAction) { throw new Error("Action is not defined as a CloudEvents action"); } const data = (objectMetadataPayload as unknown) as StorageObjectData; - return JSON.stringify({ - specVersion: 1, + let time = new Date().toISOString(); + if (data.updated) { + time = typeof data.updated === "string" ? data.updated : data.updated.toISOString(); + } + return { + specversion: "1", + id: uuid.v4(), type: `google.cloud.storage.object.v1.${ceAction}`, source: `//storage.googleapis.com/projects/_/buckets/${objectMetadataPayload.bucket}/objects/${objectMetadataPayload.name}`, + time, data, - }); + }; } } diff --git a/src/extensions/emulator/triggerHelper.ts b/src/extensions/emulator/triggerHelper.ts index 52c32f9388c..91f2526d1fb 100644 --- a/src/extensions/emulator/triggerHelper.ts +++ b/src/extensions/emulator/triggerHelper.ts @@ -10,6 +10,7 @@ export function functionResourceToEmulatedTriggerDefintion(resource: any): Parse const etd: ParsedTriggerDefinition = { name: resource.name, entryPoint: resource.name, + platform: "gcfv1", }; const properties = _.get(resource, "properties", {}); if (properties.timeout) { diff --git a/src/test/emulators/functionsEmulatorShared.spec.ts b/src/test/emulators/functionsEmulatorShared.spec.ts index bcdedf5f45c..4459322695f 100644 --- a/src/test/emulators/functionsEmulatorShared.spec.ts +++ b/src/test/emulators/functionsEmulatorShared.spec.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { getFunctionService } from "../../emulator/functionsEmulatorShared"; const baseDef = { + platform: "gcfv1" as const, id: "trigger-id", region: "us-central1", entryPoint: "fn", diff --git a/src/test/extensions/emulator/triggerHelper.spec.ts b/src/test/extensions/emulator/triggerHelper.spec.ts index 2f9bcab673b..509a3e8ac58 100644 --- a/src/test/extensions/emulator/triggerHelper.spec.ts +++ b/src/test/extensions/emulator/triggerHelper.spec.ts @@ -15,6 +15,7 @@ describe("triggerHelper", () => { }, }; const expected = { + platform: "gcfv1", availableMemoryMb: 1024, entryPoint: "test-resource", name: "test-resource", @@ -36,6 +37,7 @@ describe("triggerHelper", () => { }, }; const expected = { + platform: "gcfv1", entryPoint: "test-resource", name: "test-resource", httpsTrigger: {}, @@ -58,6 +60,7 @@ describe("triggerHelper", () => { }, }; const expected = { + platform: "gcfv1", entryPoint: "test-resource", name: "test-resource", eventTrigger: { @@ -84,6 +87,7 @@ describe("triggerHelper", () => { }, }; const expected = { + platform: "gcfv1", entryPoint: "test-resource", name: "test-resource", eventTrigger: { @@ -110,6 +114,7 @@ describe("triggerHelper", () => { }, }; const expected = { + platform: "gcfv1", entryPoint: "test-resource", name: "test-resource", eventTrigger: {