firebase-tools
Version:
Command-Line Interface for Firebase
497 lines (496 loc) • 21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AllIngressSettings = exports.AllVpcEgressSettings = exports.AllFunctionsPlatforms = exports.SECRET_REF_LONG_RE = exports.SECRET_REF_SHORT_RE = exports.GCP_SECRET_ID_PATTERN = exports.SECRET_REF_PREFIX = exports.REGION_TBD = void 0;
exports.empty = empty;
exports.of = of;
exports.isHttpsTriggered = isHttpsTriggered;
exports.isDataConnectGraphqlTriggered = isDataConnectGraphqlTriggered;
exports.isCallableTriggered = isCallableTriggered;
exports.isEventTriggered = isEventTriggered;
exports.isScheduleTriggered = isScheduleTriggered;
exports.isTaskQueueTriggered = isTaskQueueTriggered;
exports.isBlockingTriggered = isBlockingTriggered;
exports.resolveBackend = resolveBackend;
exports.envWithTypes = envWithTypes;
exports.toBackend = toBackend;
exports.applyPrefix = applyPrefix;
exports.applyEnvSecretBindings = applyEnvSecretBindings;
exports.parseSecretRef = parseSecretRef;
const backend = require("./backend");
const proto = require("../../gcp/proto");
const params = require("./params");
const logger_1 = require("../../logger");
const error_1 = require("../../error");
const functional_1 = require("../../functional");
const cel_1 = require("./cel");
exports.REGION_TBD = "REGION_TBD";
exports.SECRET_REF_PREFIX = "FIREBASE_SECRET_REF_";
const GCP_PROJECT_ID_PATTERN = "([a-z][-a-z0-9]{4,28}[a-z0-9])";
exports.GCP_SECRET_ID_PATTERN = "([a-zA-Z0-9-_]+)";
exports.SECRET_REF_SHORT_RE = new RegExp("^" +
exports.GCP_SECRET_ID_PATTERN +
"(?:[:#@]" +
"([a-z0-9-_]+)" +
")?$");
exports.SECRET_REF_LONG_RE = new RegExp("^" +
"projects/" +
GCP_PROJECT_ID_PATTERN +
"/secrets/" +
exports.GCP_SECRET_ID_PATTERN +
"(?:(?:/versions/|:|#|@)" +
"([a-z0-9-_]+)" +
")?$");
function empty() {
return {
requiredAPIs: [],
endpoints: {},
params: [],
};
}
function of(endpoints) {
const build = empty();
build.endpoints = endpoints;
return build;
}
function isHttpsTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "httpsTrigger");
}
function isDataConnectGraphqlTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "dataConnectGraphqlTrigger");
}
function isCallableTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "callableTrigger");
}
function isEventTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "eventTrigger");
}
function isScheduleTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "scheduleTrigger");
}
function isTaskQueueTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "taskQueueTrigger");
}
function isBlockingTriggered(triggered) {
return {}.hasOwnProperty.call(triggered, "blockingTrigger");
}
const allMemoryOptions = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];
exports.AllFunctionsPlatforms = ["gcfv1", "gcfv2", "run"];
exports.AllVpcEgressSettings = ["PRIVATE_RANGES_ONLY", "ALL_TRAFFIC"];
exports.AllIngressSettings = [
"ALLOW_ALL",
"ALLOW_INTERNAL_ONLY",
"ALLOW_INTERNAL_AND_GCLB",
];
async function resolveBackend(opts) {
const { paramValues: paramValues, secretRefs: secretRefs } = await params.resolveParams(opts.build.params, opts.firebaseConfig, envWithTypes(opts.build.params, opts.userEnvs), opts.codebase, opts.nonInteractive, opts.force, opts.isEmulator);
return { backend: toBackend(opts.build, paramValues), envs: paramValues, secretRefs: secretRefs };
}
function envWithTypes(definedParams, rawEnvs) {
const out = {};
for (const envName of Object.keys(rawEnvs)) {
const value = rawEnvs[envName];
let providedType = {
string: true,
boolean: true,
number: true,
list: true,
};
for (const param of definedParams) {
if (param.name === envName) {
if (param.type === "string") {
providedType = {
string: true,
boolean: false,
number: false,
list: false,
};
}
else if (param.type === "int") {
providedType = {
string: false,
boolean: false,
number: true,
list: false,
};
}
else if (param.type === "boolean") {
providedType = {
string: false,
boolean: true,
number: false,
list: false,
};
}
else if (param.type === "list") {
providedType = {
string: false,
boolean: false,
number: false,
list: true,
};
}
else if (param.type === "secret") {
providedType = {
string: true,
boolean: false,
number: false,
list: false,
};
}
}
}
out[envName] = new params.ParamValue(value, false, providedType);
}
return out;
}
class Resolver {
constructor(paramValues) {
this.paramValues = paramValues;
this.resolveInt = (i) => {
if (i === null) {
return i;
}
return params.resolveInt(i, this.paramValues);
};
this.resolveBoolean = (i) => {
if (i === null) {
return i;
}
return params.resolveBoolean(i, this.paramValues);
};
this.resolveString = (i) => {
if (i === null) {
return i;
}
return params.resolveString(i, this.paramValues);
};
}
resolveStrings(dest, src, ...keys) {
for (const key of keys) {
const orig = src[key];
if (typeof orig === "undefined") {
continue;
}
dest[key] = orig === null ? null : params.resolveString(orig, this.paramValues);
}
}
resolveInts(dest, src, ...keys) {
for (const key of keys) {
const orig = src[key];
if (typeof orig === "undefined") {
continue;
}
dest[key] = orig === null ? null : params.resolveInt(orig, this.paramValues);
}
}
}
function toBackend(build, paramValues) {
const r = new Resolver(paramValues);
const bkEndpoints = [];
for (const endpointId of Object.keys(build.endpoints)) {
const bdEndpoint = build.endpoints[endpointId];
if (r.resolveBoolean(bdEndpoint.omit || false)) {
continue;
}
let regions = [];
if (!bdEndpoint.region) {
regions = [exports.REGION_TBD];
}
else if (Array.isArray(bdEndpoint.region)) {
regions = params.resolveList(bdEndpoint.region, paramValues);
}
else {
try {
regions = params.resolveList(bdEndpoint.region, paramValues);
}
catch (err) {
if (err instanceof cel_1.ExprParseError) {
regions = [params.resolveString(bdEndpoint.region, paramValues)];
}
else {
throw err;
}
}
}
for (const region of regions) {
const trigger = discoverTrigger(bdEndpoint, region, r);
if (typeof bdEndpoint.platform === "undefined") {
throw new error_1.FirebaseError("platform can't be undefined");
}
const bkEndpoint = {
id: endpointId,
project: bdEndpoint.project,
region: region,
entryPoint: bdEndpoint.entryPoint,
platform: bdEndpoint.platform,
runtime: bdEndpoint.runtime,
...trigger,
};
proto.copyIfPresent(bkEndpoint, bdEndpoint, "environmentVariables", "labels", "secretEnvironmentVariables", "baseImageUri", "command", "args");
r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");
proto.convertIfPresent(bkEndpoint, bdEndpoint, "ingressSettings", (from) => {
if (from !== null && !backend.AllIngressSettings.includes(from)) {
throw new error_1.FirebaseError(`Cannot set ingress settings to invalid value ${from}`);
}
return from;
});
proto.convertIfPresent(bkEndpoint, bdEndpoint, "availableMemoryMb", (from) => {
const mem = r.resolveInt(from);
if (mem !== null && !backend.isValidMemoryOption(mem)) {
throw new error_1.FirebaseError(`Function memory (${mem}) must resolve to a supported value, if present: ${JSON.stringify(allMemoryOptions)}`);
}
return mem || null;
});
r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");
r.resolveInts(bkEndpoint, bdEndpoint, "timeoutSeconds", "maxInstances", "minInstances", "concurrency");
proto.convertIfPresent(bkEndpoint, bdEndpoint, "cpu", (0, functional_1.nullsafeVisitor)((cpu) => (cpu === "gcf_gen1" ? cpu : r.resolveInt(cpu))));
if (bdEndpoint.vpc) {
bkEndpoint.vpc = {};
if (typeof bdEndpoint.vpc.connector !== "undefined" && bdEndpoint.vpc.connector !== null) {
const connector = params.resolveString(bdEndpoint.vpc.connector, paramValues);
bkEndpoint.vpc.connector =
connector.includes("/") || connector === ""
? connector
: `projects/${bdEndpoint.project}/locations/${region}/connectors/${connector}`;
}
if (bdEndpoint.vpc.egressSettings) {
const egress = params.resolveString(bdEndpoint.vpc.egressSettings, paramValues);
if (!backend.AllVpcEgressSettings.includes(egress)) {
throw new error_1.FirebaseError(`Value "${egress}" is an invalid egress setting.`);
}
bkEndpoint.vpc.egressSettings = egress;
}
if (bdEndpoint.vpc.networkInterfaces) {
bkEndpoint.vpc.networkInterfaces = bdEndpoint.vpc.networkInterfaces.map((ni) => {
const resolved = {};
if (ni.network)
resolved.network = params.resolveString(ni.network, paramValues);
if (ni.subnetwork)
resolved.subnetwork = params.resolveString(ni.subnetwork, paramValues);
if (ni.tags) {
resolved.tags = ni.tags.map((tag) => params.resolveString(tag, paramValues));
}
return resolved;
});
}
}
else if (bdEndpoint.vpc === null) {
bkEndpoint.vpc = null;
}
bkEndpoints.push(bkEndpoint);
}
}
const bkend = backend.of(...bkEndpoints);
bkend.requiredAPIs = build.requiredAPIs;
if (build.requiredRoles) {
bkend.requiredRoles = build.requiredRoles;
}
if (build.lifecycleHooks) {
bkend.lifecycleHooks = build.lifecycleHooks;
}
return bkend;
}
function discoverTrigger(endpoint, region, r) {
if (isHttpsTriggered(endpoint)) {
const httpsTrigger = {};
if (endpoint.httpsTrigger.invoker === null) {
httpsTrigger.invoker = null;
}
else if (typeof endpoint.httpsTrigger.invoker !== "undefined") {
httpsTrigger.invoker = endpoint.httpsTrigger.invoker.map(r.resolveString);
}
return { httpsTrigger };
}
else if (isDataConnectGraphqlTriggered(endpoint)) {
const dataConnectGraphqlTrigger = {};
if (endpoint.dataConnectGraphqlTrigger.invoker === null) {
dataConnectGraphqlTrigger.invoker = null;
}
else if (typeof endpoint.dataConnectGraphqlTrigger.invoker !== "undefined") {
dataConnectGraphqlTrigger.invoker = endpoint.dataConnectGraphqlTrigger.invoker.map(r.resolveString);
}
proto.copyIfPresent(dataConnectGraphqlTrigger, endpoint.dataConnectGraphqlTrigger, "schemaFilePath");
return { dataConnectGraphqlTrigger };
}
else if (isCallableTriggered(endpoint)) {
const trigger = { callableTrigger: {} };
proto.copyIfPresent(trigger.callableTrigger, endpoint.callableTrigger, "genkitAction");
return trigger;
}
else if (isBlockingTriggered(endpoint)) {
return { blockingTrigger: endpoint.blockingTrigger };
}
else if (isEventTriggered(endpoint)) {
const eventTrigger = {
eventType: endpoint.eventTrigger.eventType,
retry: r.resolveBoolean(endpoint.eventTrigger.retry) || false,
};
if (endpoint.eventTrigger.eventFilters) {
eventTrigger.eventFilters = (0, functional_1.mapObject)(endpoint.eventTrigger.eventFilters, r.resolveString);
}
if (endpoint.eventTrigger.eventFilterPathPatterns) {
eventTrigger.eventFilterPathPatterns = (0, functional_1.mapObject)(endpoint.eventTrigger.eventFilterPathPatterns, r.resolveString);
}
r.resolveStrings(eventTrigger, endpoint.eventTrigger, "serviceAccount", "region", "channel");
return { eventTrigger };
}
else if (isScheduleTriggered(endpoint)) {
const bkSchedule = {
schedule: r.resolveString(endpoint.scheduleTrigger.schedule),
};
if (endpoint.scheduleTrigger.timeZone !== undefined) {
bkSchedule.timeZone = r.resolveString(endpoint.scheduleTrigger.timeZone);
}
if (endpoint.scheduleTrigger.retryConfig) {
const bkRetry = {};
r.resolveInts(bkRetry, endpoint.scheduleTrigger.retryConfig, "maxBackoffSeconds", "minBackoffSeconds", "maxRetrySeconds", "retryCount", "maxDoublings");
bkSchedule.retryConfig = bkRetry;
}
else if (endpoint.scheduleTrigger.retryConfig === null) {
bkSchedule.retryConfig = null;
}
return { scheduleTrigger: bkSchedule };
}
else if ("taskQueueTrigger" in endpoint) {
const taskQueueTrigger = {};
if (endpoint.taskQueueTrigger.rateLimits) {
taskQueueTrigger.rateLimits = {};
r.resolveInts(taskQueueTrigger.rateLimits, endpoint.taskQueueTrigger.rateLimits, "maxConcurrentDispatches", "maxDispatchesPerSecond");
}
else if (endpoint.taskQueueTrigger.rateLimits === null) {
taskQueueTrigger.rateLimits = null;
}
if (endpoint.taskQueueTrigger.retryConfig) {
taskQueueTrigger.retryConfig = {};
r.resolveInts(taskQueueTrigger.retryConfig, endpoint.taskQueueTrigger.retryConfig, "maxAttempts", "maxBackoffSeconds", "minBackoffSeconds", "maxRetrySeconds", "maxDoublings");
}
else if (endpoint.taskQueueTrigger.retryConfig === null) {
taskQueueTrigger.retryConfig = null;
}
if (endpoint.taskQueueTrigger.invoker) {
taskQueueTrigger.invoker = endpoint.taskQueueTrigger.invoker.map(r.resolveString);
}
else if (endpoint.taskQueueTrigger.invoker === null) {
taskQueueTrigger.invoker = null;
}
return { taskQueueTrigger };
}
(0, functional_1.assertExhaustive)(endpoint);
}
function applyPrefix(build, prefix) {
if (!prefix) {
return;
}
const newEndpoints = {};
for (const [id, endpoint] of Object.entries(build.endpoints)) {
const newId = `${prefix}-${id}`;
if (newId.length > 63) {
throw new error_1.FirebaseError(`Function id '${newId}' exceeds 63 characters after applying prefix '${prefix}'. Please shorten the prefix or function name.`);
}
const fnIdRegex = /^[a-zA-Z][a-zA-Z0-9_-]{0,62}$/;
if (!fnIdRegex.test(newId)) {
throw new error_1.FirebaseError(`Function id '${newId}' is invalid after applying prefix '${prefix}'. Function names must start with a letter and can contain letters, numbers, underscores, and hyphens, with a maximum length of 63 characters.`);
}
newEndpoints[newId] = endpoint;
if (endpoint.secretEnvironmentVariables) {
endpoint.secretEnvironmentVariables = endpoint.secretEnvironmentVariables.map((secret) => ({
...secret,
secret: `${prefix}-${secret.secret}`,
}));
}
}
build.endpoints = newEndpoints;
if (build.lifecycleHooks) {
for (const hook of Object.values(build.lifecycleHooks)) {
if ("task" in hook) {
if (hook.task?.function) {
hook.task.function = `${prefix}-${hook.task.function}`;
}
}
else if ("call" in hook) {
if (hook.call?.function) {
hook.call.function = `${prefix}-${hook.call.function}`;
}
}
else if ("http" in hook) {
if (hook.http?.function) {
hook.http.function = `${prefix}-${hook.http.function}`;
}
}
else {
(0, functional_1.assertExhaustive)(hook);
}
}
}
}
function applyEnvSecretBindings(build, envSecrets) {
if (envSecrets.empty) {
return;
}
logger_1.logger.debug(`Attempting to merge .env secret bindings ${JSON.stringify(envSecrets)} into declared secrets...)}`);
for (const endpointName of Object.keys(build.endpoints)) {
logger_1.logger.debug(`${endpointName} declared secrets: ${JSON.stringify(build.endpoints[endpointName].secretEnvironmentVariables)}`);
}
for (const key of Object.keys(envSecrets)) {
const secretRef = envSecrets[key];
const { projectId, secretId, version } = secretRef;
for (const param of build.params) {
if (param.type === "secret" && param.name.toUpperCase() === key) {
param.resourceId = secretId;
param.version = version;
param.inLocalEnvironment = true;
}
}
for (const endpointName of Object.keys(build.endpoints)) {
const endpoint = build.endpoints[endpointName];
if (projectId && projectId !== endpoint.project) {
throw new error_1.FirebaseError(`Secret binding ${key} referenced unsupported cross-project secret in '${projectId}'`);
}
let notFound = true;
for (const envVar of endpoint.secretEnvironmentVariables ?? []) {
if (envVar.key === key) {
notFound = false;
envVar.secret = secretId;
if (typeof secretRef.version !== "undefined") {
envVar.version = version;
envVar.allowVersionPinning = true;
}
logger_1.logger.debug(`Merged secret: ${JSON.stringify(envVar)}`);
}
}
if (notFound) {
logger_1.logger.warn(`.env files contain a secret binding ${key} which has not been configured as a secret param via defineSecret().`);
}
}
}
}
function parseSecretRef(ref) {
const shortMatch = exports.SECRET_REF_SHORT_RE.exec(ref);
if (shortMatch) {
const output = {
secretId: shortMatch[1],
};
if (shortMatch[2] !== undefined) {
output.version = shortMatch[2];
if (ref.includes("#") || ref.includes("@")) {
throw new error_1.FirebaseError(`Malformed secret binding '${ref}'; secret versions are specified with ':'`);
}
}
return output;
}
const longMatch = exports.SECRET_REF_LONG_RE.exec(ref);
if (longMatch) {
const output = {
projectId: longMatch[1],
secretId: longMatch[2],
};
if (longMatch[3] !== undefined) {
output.version = longMatch[3];
if (ref.includes("#") || ref.includes("@")) {
throw new error_1.FirebaseError(`Malformed secret binding '${ref}'; secret versions are specified with ':'`);
}
}
return output;
}
throw new error_1.FirebaseError(`Unknown format for secret binding '${ref}'`);
}