Skip to content
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

Emulator bugfixes #3887

Merged
merged 15 commits into from Dec 14, 2021
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 @@ -493,6 +494,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
32 changes: 22 additions & 10 deletions src/emulator/storage/cloudFunctions.ts
Expand Up @@ -4,6 +4,8 @@ import { EmulatorLogger } from "../emulatorLogger";
import { CloudStorageObjectMetadata, toSerializedDate } from "./metadata";
import { Client } from "../../apiv2";
import { StorageObjectData } from "@google/events/cloud/storage/v1/StorageObjectData";
import { CloudEvent } from "../events/types";
import uuid from "uuid";

type StorageCloudFunctionAction = "finalize" | "metadataUpdate" | "delete" | "archive";
const STORAGE_V2_ACTION_MAP: Record<StorageCloudFunctionAction, string> = {
Expand Down Expand Up @@ -54,9 +56,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 +83,9 @@ export class StorageCloudFunctions {
private createLegacyEventRequestBody(
action: StorageCloudFunctionAction,
objectMetadataPayload: ObjectMetadataPayload
): string {
) {
taeold marked this conversation as resolved.
Show resolved Hide resolved
const timestamp = new Date();
return JSON.stringify({
return {
eventId: `${timestamp.getTime()}`,
timestamp: toSerializedDate(timestamp),
eventType: `google.storage.object.${action}`,
Expand All @@ -89,25 +95,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 definied 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