Skip to content

Commit

Permalink
Emulator bugfixes (#3887)
Browse files Browse the repository at this point in the history
1. Functions emulator now checks that the function names are valid before loading them.
2. Fixes up the incomplete CloudEvent payload for Storage and Pub/Sub
  • Loading branch information
taeold committed Dec 14, 2021
1 parent f953698 commit 6cebe6a
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 76 deletions.
5 changes: 5 additions & 0 deletions scripts/emulator-tests/functionsEmulator.spec.ts
Expand Up @@ -37,6 +37,7 @@ functionsEmulator.nodeBinary = process.execPath;

functionsEmulator.setTriggersForTesting([
{
platform: "gcfv1",
name: "function_id",
id: "us-central1-function_id",
region: "us-central1",
Expand All @@ -45,6 +46,7 @@ functionsEmulator.setTriggersForTesting([
labels: {},
},
{
platform: "gcfv1",
name: "function_id",
id: "europe-west2-function_id",
region: "europe-west2",
Expand All @@ -53,6 +55,7 @@ functionsEmulator.setTriggersForTesting([
labels: {},
},
{
platform: "gcfv1",
name: "function_id",
id: "europe-west3-function_id",
region: "europe-west3",
Expand All @@ -61,6 +64,7 @@ functionsEmulator.setTriggersForTesting([
labels: {},
},
{
platform: "gcfv1",
name: "callable_function_id",
id: "us-central1-callable_function_id",
region: "us-central1",
Expand All @@ -71,6 +75,7 @@ functionsEmulator.setTriggersForTesting([
},
},
{
platform: "gcfv1",
name: "nested-function_id",
id: "us-central1-nested-function_id",
region: "us-central1",
Expand Down
13 changes: 13 additions & 0 deletions src/emulator/functionsEmulator.ts
Expand Up @@ -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";

Expand Down Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion src/emulator/functionsEmulatorShared.ts
Expand Up @@ -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[];
Expand All @@ -21,7 +22,6 @@ export interface ParsedTriggerDefinition {
eventTrigger?: EventTrigger;
schedule?: EventSchedule;
labels?: { [key: string]: any };
platform?: FunctionsPlatform;
}

export interface EmulatedTriggerDefinition extends ParsedTriggerDefinition {
Expand Down Expand Up @@ -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);
}
Expand Down
137 changes: 72 additions & 65 deletions src/emulator/pubsubEmulator.ts
Expand Up @@ -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;
Expand All @@ -32,6 +33,9 @@ export class PubsubEmulator implements EmulatorInstance {
// Map of topic name to a PubSub subscription object
subscriptionForTopic: Map<string, Subscription>;

// Client for communicating with the Functions Emulator
private client?: Client;

private logger = EmulatorLogger.forEmulator(Emulators.PUBSUB);

constructor(private args: PubsubEmulatorArgs) {
Expand All @@ -40,7 +44,6 @@ export class PubsubEmulator implements EmulatorInstance {
apiEndpoint: `${host}:${port}`,
projectId: this.args.projectId,
});

this.triggersForTopic = new Map();
this.subscriptionForTopic = new Map();
}
Expand Down Expand Up @@ -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<string, unknown> {
const baseOpts = {
origin: `http://${EmulatorRegistry.getInfoHostString(
EmulatorRegistry.get(Emulators.FUNCTIONS)!.getInfo()
)}`,
message: Message
): CloudEvent<MessagePublishedData> {
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) {
Expand All @@ -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",
Expand All @@ -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<CloudEvent<MessagePublishedData>, 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);
}
Expand Down
33 changes: 23 additions & 10 deletions 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<StorageCloudFunctionAction, string> = {
Expand Down Expand Up @@ -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<CloudEvent<StorageObjectData>, any>(
this.multicastPath,
cloudEventBody,
{
headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" },
}
);
if (cloudEventRes.status !== 200) {
errStatus.push(cloudEventRes.status);
}
Expand All @@ -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}`,
Expand All @@ -89,25 +96,31 @@ export class StorageCloudFunctions {
type: "storage#object",
}, // bucket
data: objectMetadataPayload,
});
};
}

/** Modern CloudEvents type */
private createCloudEventRequestBody(
action: StorageCloudFunctionAction,
objectMetadataPayload: ObjectMetadataPayload
): string {
): CloudEvent<StorageObjectData> {
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,
});
};
}
}

Expand Down
1 change: 1 addition & 0 deletions src/extensions/emulator/triggerHelper.ts
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/test/emulators/functionsEmulatorShared.spec.ts
Expand Up @@ -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",
Expand Down

0 comments on commit 6cebe6a

Please sign in to comment.