firebase-tools
Version:
Command-Line Interface for Firebase
333 lines (332 loc) • 15.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.addResourcesToBackend = exports.addResourcesToBuild = exports.mergeRequiredAPIs = exports.discoverBackend = exports.discoverBuild = exports.useStrategy = void 0;
const path = require("path");
const _ = require("lodash");
const child_process_1 = require("child_process");
const error_1 = require("../../../../error");
const logger_1 = require("../../../../logger");
const backend = require("../../backend");
const api = require("../../../../api");
const proto = require("../../../../gcp/proto");
const events = require("../../../../functions/events");
const functional_1 = require("../../../../functional");
const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");
function removeInspectOptions(options) {
return options.filter((opt) => !opt.startsWith("--inspect"));
}
function parseTriggers(projectId, sourceDir, configValues, envs) {
return new Promise((resolve, reject) => {
const env = Object.assign({}, envs);
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 = (0, child_process_1.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 error_1.FirebaseError(message.error, { exit: 1 }));
}
});
parser.on("exit", (code) => {
if (code !== 0) {
reject(new error_1.FirebaseError("There was an unknown problem while trying to parse function triggers.", { exit: 2 }));
}
});
});
}
function useStrategy() {
return Promise.resolve(true);
}
exports.useStrategy = useStrategy;
async function discoverBuild(projectId, sourceDir, runtime, configValues, envs) {
const triggerAnnotations = await parseTriggers(projectId, sourceDir, configValues, envs);
const want = {
requiredAPIs: [],
endpoints: {},
params: [],
};
for (const annotation of triggerAnnotations) {
addResourcesToBuild(projectId, runtime, annotation, want);
}
return want;
}
exports.discoverBuild = discoverBuild;
async function discoverBackend(projectId, sourceDir, runtime, configValues, envs) {
const triggerAnnotations = await parseTriggers(projectId, sourceDir, configValues, envs);
const want = Object.assign(Object.assign({}, backend.empty()), { environmentVariables: envs });
for (const annotation of triggerAnnotations) {
addResourcesToBackend(projectId, runtime, annotation, want);
}
return want;
}
exports.discoverBackend = discoverBackend;
function mergeRequiredAPIs(backend) {
const apiToReasons = {};
for (const { api, reason } of backend.requiredAPIs) {
const reasons = apiToReasons[api] || new Set();
if (reason) {
reasons.add(reason);
}
apiToReasons[api] = reasons;
}
const merged = [];
for (const [api, reasons] of Object.entries(apiToReasons)) {
merged.push({ api, reason: Array.from(reasons).join(" ") });
}
backend.requiredAPIs = merged;
}
exports.mergeRequiredAPIs = mergeRequiredAPIs;
function addResourcesToBuild(projectId, runtime, annotation, want) {
var _a, _b;
Object.freeze(annotation);
const toSeconds = (0, functional_1.nullsafeVisitor)(proto.secondsFromDuration);
const regions = annotation.regions || [api.functionsDefaultRegion()];
let triggered;
const triggerCount = +!!annotation.httpsTrigger +
+!!annotation.eventTrigger +
+!!annotation.taskQueueTrigger +
+!!annotation.blockingTrigger;
if (triggerCount !== 1) {
throw new error_1.FirebaseError("Unexpected annotation generated by the Firebase Functions SDK. This should never happen.");
}
if (annotation.taskQueueTrigger) {
want.requiredAPIs.push({
api: "cloudtasks.googleapis.com",
reason: "Needed for task queue functions.",
});
triggered = {
taskQueueTrigger: {},
};
proto.copyIfPresent(triggered.taskQueueTrigger, annotation.taskQueueTrigger, "invoker");
proto.copyIfPresent(triggered.taskQueueTrigger, annotation.taskQueueTrigger, "rateLimits");
if (annotation.taskQueueTrigger.retryConfig) {
triggered.taskQueueTrigger.retryConfig = {};
proto.copyIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "maxAttempts", "maxDoublings");
proto.convertIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "minBackoffSeconds", "minBackoff", toSeconds);
proto.convertIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "maxBackoffSeconds", "maxBackoff", toSeconds);
proto.convertIfPresent(triggered.taskQueueTrigger.retryConfig, annotation.taskQueueTrigger.retryConfig, "maxRetrySeconds", "maxRetryDuration", toSeconds);
}
}
else if (annotation.httpsTrigger) {
if ((_a = annotation.labels) === null || _a === void 0 ? void 0 : _a["deployment-callable"]) {
delete annotation.labels["deployment-callable"];
triggered = { callableTrigger: {} };
}
else {
const trigger = {};
if (annotation.failurePolicy) {
logger_1.logger.warn(`Ignoring retry policy for HTTPS function ${annotation.name}`);
}
if (annotation.httpsTrigger.invoker) {
trigger.invoker = annotation.httpsTrigger.invoker;
}
triggered = { httpsTrigger: trigger };
}
}
else if (annotation.schedule) {
want.requiredAPIs.push({
api: "cloudscheduler.googleapis.com",
reason: "Needed for scheduled functions.",
});
triggered = {
scheduleTrigger: {
schedule: annotation.schedule.schedule,
timeZone: (_b = annotation.schedule.timeZone) !== null && _b !== void 0 ? _b : null,
retryConfig: {},
},
};
if (annotation.schedule.retryConfig) {
triggered.scheduleTrigger.retryConfig = {};
proto.copyIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "retryCount", "maxDoublings");
proto.convertIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "maxRetrySeconds", "maxRetryDuration", toSeconds);
proto.convertIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "minBackoffSeconds", "minBackoffDuration", toSeconds);
proto.convertIfPresent(triggered.scheduleTrigger.retryConfig, annotation.schedule.retryConfig, "maxBackoffSeconds", "maxBackoffDuration", toSeconds);
}
}
else if (annotation.blockingTrigger) {
if (events.v1.AUTH_BLOCKING_EVENTS.includes(annotation.blockingTrigger.eventType)) {
want.requiredAPIs.push({
api: "identitytoolkit.googleapis.com",
reason: "Needed for auth blocking functions.",
});
}
triggered = {
blockingTrigger: {
eventType: annotation.blockingTrigger.eventType,
},
};
}
else if (annotation.eventTrigger) {
triggered = {
eventTrigger: {
eventType: annotation.eventTrigger.eventType,
eventFilters: { resource: annotation.eventTrigger.resource },
retry: !!annotation.failurePolicy,
},
};
}
else {
throw new error_1.FirebaseError("Do not understand Cloud Function annotation without a trigger" +
JSON.stringify(annotation, null, 2));
}
const endpointId = annotation.name;
const endpoint = Object.assign({ platform: annotation.platform || "gcfv1", region: regions, project: projectId, entryPoint: annotation.entryPoint, runtime: runtime }, triggered);
proto.renameIfPresent(endpoint, annotation, "serviceAccount", "serviceAccountEmail");
if (annotation.vpcConnector != null) {
endpoint.vpc = { connector: annotation.vpcConnector };
proto.renameIfPresent(endpoint.vpc, annotation, "egressSettings", "vpcConnectorEgressSettings");
}
proto.copyIfPresent(endpoint, annotation, "concurrency", "labels", "maxInstances", "minInstances", "availableMemoryMb");
proto.convertIfPresent(endpoint, annotation, "ingressSettings", (str) => {
if (str === null) {
return null;
}
if (!backend.AllIngressSettings.includes(str)) {
throw new Error(`Invalid ingress setting ${str}`);
}
return str;
});
proto.convertIfPresent(endpoint, annotation, "timeoutSeconds", "timeout", proto.secondsFromDuration);
if (annotation.secrets) {
endpoint.secretEnvironmentVariables = annotation.secrets.map((secret) => {
return {
secret,
projectId,
key: secret,
};
});
}
want.endpoints[endpointId] = endpoint;
}
exports.addResourcesToBuild = addResourcesToBuild;
function addResourcesToBackend(projectId, runtime, annotation, want) {
var _a;
Object.freeze(annotation);
for (const region of annotation.regions || [api.functionsDefaultRegion()]) {
let triggered;
const triggerCount = +!!annotation.httpsTrigger +
+!!annotation.eventTrigger +
+!!annotation.taskQueueTrigger +
+!!annotation.blockingTrigger;
if (triggerCount !== 1) {
throw new error_1.FirebaseError("Unexpected annotation generated by the Firebase Functions SDK. This should never happen.");
}
if (annotation.taskQueueTrigger) {
triggered = { taskQueueTrigger: annotation.taskQueueTrigger };
want.requiredAPIs.push({
api: "cloudtasks.googleapis.com",
reason: "Needed for task queue functions.",
});
}
else if (annotation.httpsTrigger) {
if ((_a = annotation.labels) === null || _a === void 0 ? void 0 : _a["deployment-callable"]) {
delete annotation.labels["deployment-callable"];
triggered = { callableTrigger: {} };
}
else {
const trigger = {};
if (annotation.failurePolicy) {
logger_1.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.push({
api: "cloudscheduler.googleapis.com",
reason: "Needed for scheduled functions.",
});
triggered = { scheduleTrigger: annotation.schedule };
}
else if (annotation.blockingTrigger) {
if (events.v1.AUTH_BLOCKING_EVENTS.includes(annotation.blockingTrigger.eventType)) {
want.requiredAPIs.push({
api: "identitytoolkit.googleapis.com",
reason: "Needed for auth blocking functions.",
});
}
triggered = {
blockingTrigger: {
eventType: annotation.blockingTrigger.eventType,
options: annotation.blockingTrigger.options,
},
};
}
else {
triggered = {
eventTrigger: {
eventType: annotation.eventTrigger.eventType,
eventFilters: { resource: annotation.eventTrigger.resource },
retry: !!annotation.failurePolicy,
},
};
if (annotation.platform === "gcfv2") {
if (annotation.eventTrigger.eventType === events.v2.PUBSUB_PUBLISH_EVENT) {
triggered.eventTrigger.eventFilters = { topic: annotation.eventTrigger.resource };
}
if (events.v2.STORAGE_EVENTS.find((event) => { var _a; return event === (((_a = annotation.eventTrigger) === null || _a === void 0 ? void 0 : _a.eventType) || ""); })) {
triggered.eventTrigger.eventFilters = { bucket: annotation.eventTrigger.resource };
}
}
}
const endpoint = Object.assign({ platform: annotation.platform || "gcfv1", id: annotation.name, region: region, project: projectId, entryPoint: annotation.entryPoint, runtime: runtime }, triggered);
if (annotation.vpcConnector != null) {
let maybeId = annotation.vpcConnector;
if (maybeId && !maybeId.includes("/")) {
maybeId = `projects/${projectId}/locations/${region}/connectors/${maybeId}`;
}
endpoint.vpc = { connector: maybeId };
proto.renameIfPresent(endpoint.vpc, annotation, "egressSettings", "vpcConnectorEgressSettings");
}
if (annotation.secrets) {
const secretEnvs = [];
for (const secret of annotation.secrets) {
const secretEnv = {
secret,
projectId,
key: secret,
};
secretEnvs.push(secretEnv);
}
endpoint.secretEnvironmentVariables = secretEnvs;
}
proto.copyIfPresent(endpoint, annotation, "concurrency", "labels", "maxInstances", "minInstances");
proto.renameIfPresent(endpoint, annotation, "serviceAccount", "serviceAccountEmail");
proto.convertIfPresent(endpoint, annotation, "ingressSettings", (ingress) => {
if (ingress == null) {
return null;
}
if (!backend.AllIngressSettings.includes(ingress)) {
throw new error_1.FirebaseError(`Invalid ingress setting ${ingress}`);
}
return ingress;
});
proto.convertIfPresent(endpoint, annotation, "availableMemoryMb", (mem) => {
if (mem === null) {
return null;
}
if (!backend.isValidMemoryOption(mem)) {
throw new error_1.FirebaseError(`This version of firebase-tools does not know about the memory option ${mem}. Is an upgrade necessary?`);
}
return mem;
});
proto.convertIfPresent(endpoint, annotation, "timeoutSeconds", "timeout", proto.secondsFromDuration);
want.endpoints[region] = want.endpoints[region] || {};
want.endpoints[region][endpoint.id] = endpoint;
mergeRequiredAPIs(want);
}
}
exports.addResourcesToBackend = addResourcesToBackend;
;