-
Notifications
You must be signed in to change notification settings - Fork 901
/
parseTriggers.ts
244 lines (226 loc) · 7.37 KB
/
parseTriggers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import * as path from "path";
import * as _ from "lodash";
import { fork } from "child_process";
import { FirebaseError } from "../../../../error";
import { logger } from "../../../../logger";
import * as backend from "../../backend";
import * as api from "../../../../api";
import * as proto from "../../../../gcp/proto";
import * as args from "../../args";
import * as runtimes from "../../runtimes";
const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");
export const GCS_EVENTS: Set<string> = new Set<string>([
"google.cloud.storage.object.v1.finalized",
"google.cloud.storage.object.v1.archived",
"google.cloud.storage.object.v1.deleted",
"google.cloud.storage.object.v1.metadataUpdated",
]);
export interface ScheduleRetryConfig {
retryCount?: number;
maxRetryDuration?: string;
minBackoffDuration?: string;
maxBackoffDuration?: string;
maxDoublings?: number;
}
/**
* Configuration options for scheduled functions.
*/
export interface ScheduleAnnotation {
schedule: string;
timeZone?: string;
retryConfig?: ScheduleRetryConfig;
}
// Defined in firebase-functions/src/cloud-function.ts
export interface TriggerAnnotation {
name: string;
platform?: "gcfv1" | "gcfv2";
labels?: Record<string, string>;
entryPoint: string;
vpcConnector?: string;
vpcConnectorEgressSettings?: string;
ingressSettings?: string;
availableMemoryMb?: number;
timeout?: proto.Duration;
maxInstances?: number;
minInstances?: number;
serviceAccountEmail?: string;
httpsTrigger?: {
invoker?: string[];
};
eventTrigger?: {
eventType: string;
resource: string;
// Deprecated
service: string;
};
taskQueueTrigger?: {
rateLimits?: {
maxBurstSize?: number;
maxConcurrentDispatches?: number;
maxDispatchesPerSecond?: number;
};
retryConfig?: {
maxAttempts?: number;
maxRetryDuration?: proto.Duration;
minBackoff?: proto.Duration;
maxBackoff?: proto.Duration;
maxDoublings?: number;
};
invoker?: string[];
};
failurePolicy?: {};
schedule?: ScheduleAnnotation;
timeZone?: string;
regions?: string[];
concurrency?: number;
}
/**
* Removes any inspect options (`inspect` or `inspect-brk`) from options so the forked process is able to run (otherwise
* it'll inherit process values and will use the same port).
* @param options From either `process.execArgv` or `NODE_OPTIONS` envar (which is a space separated string)
* @return `options` without any `inspect` or `inspect-brk` values
*/
function removeInspectOptions(options: string[]): string[] {
return options.filter((opt) => !opt.startsWith("--inspect"));
}
function parseTriggers(
projectId: string,
sourceDir: string,
configValues: backend.RuntimeConfigValues,
envs: backend.EnvironmentVariables
): Promise<TriggerAnnotation[]> {
return new Promise((resolve, reject) => {
const env = { ...envs } as NodeJS.ProcessEnv;
env.GCLOUD_PROJECT = projectId;
if (!_.isEmpty(configValues)) {
env.CLOUD_RUNTIME_CONFIG = JSON.stringify(configValues);
}
const execArgv = removeInspectOptions(process.execArgv);
if (env.NODE_OPTIONS) {
env.NODE_OPTIONS = removeInspectOptions(env.NODE_OPTIONS.split(" ")).join(" ");
}
const parser = fork(TRIGGER_PARSER, [sourceDir], {
silent: true,
env: env,
execArgv: execArgv,
});
parser.on("message", (message) => {
if (message.triggers) {
resolve(message.triggers);
} else if (message.error) {
reject(new FirebaseError(message.error, { exit: 1 }));
}
});
parser.on("exit", (code) => {
if (code !== 0) {
reject(
new FirebaseError(
"There was an unknown problem while trying to parse function triggers.",
{ exit: 2 }
)
);
}
});
});
}
// Currently we always use JS trigger parsing
export function useStrategy(context: args.Context): Promise<boolean> {
return Promise.resolve(true);
}
export async function discoverBackend(
projectId: string,
sourceDir: string,
runtime: runtimes.Runtime,
configValues: backend.RuntimeConfigValues,
envs: backend.EnvironmentVariables
): Promise<backend.Backend> {
const triggerAnnotations = await parseTriggers(projectId, sourceDir, configValues, envs);
const want: backend.Backend = { ...backend.empty(), environmentVariables: envs };
for (const annotation of triggerAnnotations) {
addResourcesToBackend(projectId, runtime, annotation, want);
}
return want;
}
export function addResourcesToBackend(
projectId: string,
runtime: runtimes.Runtime,
annotation: TriggerAnnotation,
want: backend.Backend
): void {
Object.freeze(annotation);
// Every trigger annotation is at least a function
for (const region of annotation.regions || [api.functionsDefaultRegion]) {
let triggered: backend.Triggered;
// +!! is 1 for truthy values and 0 for falsy values
const triggerCount =
+!!annotation.httpsTrigger + +!!annotation.eventTrigger + +!!annotation.taskQueueTrigger;
if (triggerCount != 1) {
throw new FirebaseError(
"Unexpected annotation generated by the Firebase Functions SDK. This should never happen."
);
}
if (annotation.taskQueueTrigger) {
triggered = { taskQueueTrigger: annotation.taskQueueTrigger };
want.requiredAPIs["cloudtasks"] = "cloudtasks.googleapis.com";
} else if (annotation.httpsTrigger) {
const trigger: backend.HttpsTrigger = {};
if (annotation.failurePolicy) {
logger.warn(`Ignoring retry policy for HTTPS function ${annotation.name}`);
}
proto.copyIfPresent(trigger, annotation.httpsTrigger, "invoker");
triggered = { httpsTrigger: trigger };
} else if (annotation.schedule) {
want.requiredAPIs["pubsub"] = "pubsub.googleapis.com";
want.requiredAPIs["scheduler"] = "cloudscheduler.googleapis.com";
triggered = { scheduleTrigger: annotation.schedule };
} else {
triggered = {
eventTrigger: {
eventType: annotation.eventTrigger!.eventType,
eventFilters: {
resource: annotation.eventTrigger!.resource,
},
retry: !!annotation.failurePolicy,
},
};
// TODO: yank this edge case for a v2 trigger on the pre-container contract
// once we use container contract for the functionsv2 experiment.
if (GCS_EVENTS.has(annotation.eventTrigger?.eventType || "")) {
triggered.eventTrigger.eventFilters = {
bucket: annotation.eventTrigger!.resource,
};
}
}
const endpoint: backend.Endpoint = {
platform: annotation.platform || "gcfv1",
id: annotation.name,
region: region,
project: projectId,
entryPoint: annotation.entryPoint,
runtime: runtime,
...triggered,
};
if (annotation.vpcConnector) {
let maybeId = annotation.vpcConnector;
if (!maybeId.includes("/")) {
maybeId = `projects/${projectId}/locations/${region}/connectors/${maybeId}`;
}
endpoint.vpcConnector = maybeId;
}
proto.copyIfPresent(
endpoint,
annotation,
"concurrency",
"serviceAccountEmail",
"labels",
"vpcConnectorEgressSettings",
"ingressSettings",
"timeout",
"maxInstances",
"minInstances",
"availableMemoryMb"
);
want.endpoints[region] = want.endpoints[region] || {};
want.endpoints[region][endpoint.id] = endpoint;
}
}