You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
517 lines
20 KiB
517 lines
20 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.resolveParams = exports.ParamValue = exports.isMultiSelectInput = exports.isResourceInput = exports.isSelectInput = exports.isTextInput = exports.resolveBoolean = exports.resolveList = exports.resolveString = exports.resolveInt = void 0;
|
|
const logger_1 = require("../../logger");
|
|
const error_1 = require("../../error");
|
|
const prompt_1 = require("../../prompt");
|
|
const functional_1 = require("../../functional");
|
|
const secretManager = require("../../gcp/secretManager");
|
|
const storage_1 = require("../../gcp/storage");
|
|
const cel_1 = require("./cel");
|
|
function dependenciesCEL(expr) {
|
|
const deps = [];
|
|
const paramCapture = /{{ params\.(\w+) }}/g;
|
|
let match;
|
|
while ((match = paramCapture.exec(expr)) != null) {
|
|
deps.push(match[1]);
|
|
}
|
|
return deps;
|
|
}
|
|
function resolveInt(from, paramValues) {
|
|
if (typeof from === "number") {
|
|
return from;
|
|
}
|
|
return (0, cel_1.resolveExpression)("number", from, paramValues);
|
|
}
|
|
exports.resolveInt = resolveInt;
|
|
function resolveString(from, paramValues) {
|
|
let output = from;
|
|
const celCapture = /{{ .+? }}/g;
|
|
const subExprs = from.match(celCapture);
|
|
if (!subExprs || subExprs.length === 0) {
|
|
return output;
|
|
}
|
|
for (const expr of subExprs) {
|
|
const resolved = (0, cel_1.resolveExpression)("string", expr, paramValues);
|
|
output = output.replace(expr, resolved);
|
|
}
|
|
return output;
|
|
}
|
|
exports.resolveString = resolveString;
|
|
function resolveList(from, paramValues) {
|
|
if (!from) {
|
|
return [];
|
|
}
|
|
else if (Array.isArray(from)) {
|
|
return from.map((entry) => resolveString(entry, paramValues));
|
|
}
|
|
else if (typeof from === "string") {
|
|
return (0, cel_1.resolveExpression)("string[]", from, paramValues);
|
|
}
|
|
else {
|
|
(0, functional_1.assertExhaustive)(from);
|
|
}
|
|
}
|
|
exports.resolveList = resolveList;
|
|
function resolveBoolean(from, paramValues) {
|
|
if (typeof from === "boolean") {
|
|
return from;
|
|
}
|
|
return (0, cel_1.resolveExpression)("boolean", from, paramValues);
|
|
}
|
|
exports.resolveBoolean = resolveBoolean;
|
|
function isTextInput(input) {
|
|
return {}.hasOwnProperty.call(input, "text");
|
|
}
|
|
exports.isTextInput = isTextInput;
|
|
function isSelectInput(input) {
|
|
return {}.hasOwnProperty.call(input, "select");
|
|
}
|
|
exports.isSelectInput = isSelectInput;
|
|
function isResourceInput(input) {
|
|
return {}.hasOwnProperty.call(input, "resource");
|
|
}
|
|
exports.isResourceInput = isResourceInput;
|
|
function isMultiSelectInput(input) {
|
|
return {}.hasOwnProperty.call(input, "multiSelect");
|
|
}
|
|
exports.isMultiSelectInput = isMultiSelectInput;
|
|
class ParamValue {
|
|
constructor(rawValue, internal, types) {
|
|
this.rawValue = rawValue;
|
|
this.internal = internal;
|
|
this.legalString = types.string || false;
|
|
this.legalBoolean = types.boolean || false;
|
|
this.legalNumber = types.number || false;
|
|
this.legalList = types.list || false;
|
|
this.delimiter = ",";
|
|
}
|
|
static fromList(ls, delimiter = ",") {
|
|
const pv = new ParamValue(ls.join(delimiter), false, { list: true });
|
|
pv.setDelimiter(delimiter);
|
|
return pv;
|
|
}
|
|
setDelimiter(delimiter) {
|
|
this.delimiter = delimiter;
|
|
}
|
|
toString() {
|
|
return this.rawValue;
|
|
}
|
|
toSDK() {
|
|
return this.legalList ? JSON.stringify(this.asList()) : this.toString();
|
|
}
|
|
asString() {
|
|
return this.rawValue;
|
|
}
|
|
asBoolean() {
|
|
return ["true", "y", "yes", "1"].includes(this.rawValue);
|
|
}
|
|
asList() {
|
|
return this.rawValue.split(this.delimiter);
|
|
}
|
|
asNumber() {
|
|
return +this.rawValue;
|
|
}
|
|
}
|
|
exports.ParamValue = ParamValue;
|
|
function resolveDefaultCEL(type, expr, currentEnv) {
|
|
const deps = dependenciesCEL(expr);
|
|
const allDepsFound = deps.every((dep) => !!currentEnv[dep]);
|
|
if (!allDepsFound) {
|
|
throw new error_1.FirebaseError("Build specified parameter with un-resolvable default value " +
|
|
expr +
|
|
"; dependencies missing.");
|
|
}
|
|
switch (type) {
|
|
case "boolean":
|
|
return resolveBoolean(expr, currentEnv);
|
|
case "string":
|
|
return resolveString(expr, currentEnv);
|
|
case "int":
|
|
return resolveInt(expr, currentEnv);
|
|
case "list":
|
|
return resolveList(expr, currentEnv);
|
|
default:
|
|
throw new error_1.FirebaseError("Build specified parameter with default " + expr + " of unsupported type");
|
|
}
|
|
}
|
|
function canSatisfyParam(param, value) {
|
|
if (param.type === "string") {
|
|
return typeof value === "string";
|
|
}
|
|
else if (param.type === "int") {
|
|
return typeof value === "number" && Number.isInteger(value);
|
|
}
|
|
else if (param.type === "boolean") {
|
|
return typeof value === "boolean";
|
|
}
|
|
else if (param.type === "list") {
|
|
return Array.isArray(value);
|
|
}
|
|
else if (param.type === "secret") {
|
|
return false;
|
|
}
|
|
(0, functional_1.assertExhaustive)(param);
|
|
}
|
|
async function resolveParams(params, firebaseConfig, userEnvs, nonInteractive) {
|
|
const paramValues = populateDefaultParams(firebaseConfig);
|
|
const [resolved, outstanding] = (0, functional_1.partition)(params, (param) => {
|
|
return {}.hasOwnProperty.call(userEnvs, param.name);
|
|
});
|
|
for (const param of resolved) {
|
|
paramValues[param.name] = userEnvs[param.name];
|
|
}
|
|
const [needSecret, needPrompt] = (0, functional_1.partition)(outstanding, (param) => param.type === "secret");
|
|
for (const param of needSecret) {
|
|
await handleSecret(param, firebaseConfig.projectId);
|
|
}
|
|
if (nonInteractive && needPrompt.length > 0) {
|
|
const envNames = outstanding.map((p) => p.name).join(", ");
|
|
throw new error_1.FirebaseError(`In non-interactive mode but have no value for the following environment variables: ${envNames}\n` +
|
|
"To continue, either run `firebase deploy` with an interactive terminal, or add values to a dotenv file. " +
|
|
"For information regarding how to use dotenv files, see https://firebase.google.com/docs/functions/config-env");
|
|
}
|
|
for (const param of needPrompt) {
|
|
const promptable = param;
|
|
let paramDefault = promptable.default;
|
|
if (paramDefault && (0, cel_1.isCelExpression)(paramDefault)) {
|
|
paramDefault = resolveDefaultCEL(param.type, paramDefault, paramValues);
|
|
}
|
|
if (paramDefault && !canSatisfyParam(param, paramDefault)) {
|
|
throw new error_1.FirebaseError("Parameter " + param.name + " has default value " + paramDefault + " of wrong type");
|
|
}
|
|
paramValues[param.name] = await promptParam(param, firebaseConfig.projectId, paramDefault);
|
|
}
|
|
return paramValues;
|
|
}
|
|
exports.resolveParams = resolveParams;
|
|
function populateDefaultParams(config) {
|
|
const defaultParams = {};
|
|
if (config.databaseURL && config.databaseURL !== "") {
|
|
defaultParams["DATABASE_URL"] = new ParamValue(config.databaseURL, true, {
|
|
string: true,
|
|
boolean: false,
|
|
number: false,
|
|
});
|
|
}
|
|
defaultParams["PROJECT_ID"] = new ParamValue(config.projectId, true, {
|
|
string: true,
|
|
boolean: false,
|
|
number: false,
|
|
});
|
|
defaultParams["GCLOUD_PROJECT"] = new ParamValue(config.projectId, true, {
|
|
string: true,
|
|
boolean: false,
|
|
number: false,
|
|
});
|
|
if (config.storageBucket && config.storageBucket !== "") {
|
|
defaultParams["STORAGE_BUCKET"] = new ParamValue(config.storageBucket, true, {
|
|
string: true,
|
|
boolean: false,
|
|
number: false,
|
|
});
|
|
}
|
|
return defaultParams;
|
|
}
|
|
async function handleSecret(secretParam, projectId) {
|
|
const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest");
|
|
if (!metadata.secret) {
|
|
const secretValue = await (0, prompt_1.promptOnce)({
|
|
name: secretParam.name,
|
|
type: "password",
|
|
message: `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${secretParam.name}. Enter a value for ${secretParam.label || secretParam.name}:`,
|
|
});
|
|
const secretLabel = { "firebase-hosting-managed": "yes" };
|
|
await secretManager.createSecret(projectId, secretParam.name, secretLabel);
|
|
await secretManager.addVersion(projectId, secretParam.name, secretValue);
|
|
return secretValue;
|
|
}
|
|
else if (!metadata.secretVersion) {
|
|
throw new error_1.FirebaseError(`Cloud Secret Manager has no latest version of the secret defined by param ${secretParam.label || secretParam.name}`);
|
|
}
|
|
else if (metadata.secretVersion.state === "DESTROYED" ||
|
|
metadata.secretVersion.state === "DISABLED") {
|
|
throw new error_1.FirebaseError(`Cloud Secret Manager's latest version of secret '${secretParam.label || secretParam.name} is in illegal state ${metadata.secretVersion.state}`);
|
|
}
|
|
}
|
|
async function promptParam(param, projectId, resolvedDefault) {
|
|
if (param.type === "string") {
|
|
const provided = await promptStringParam(param, projectId, resolvedDefault);
|
|
return new ParamValue(provided.toString(), false, { string: true });
|
|
}
|
|
else if (param.type === "int") {
|
|
const provided = await promptIntParam(param, resolvedDefault);
|
|
return new ParamValue(provided.toString(), false, { number: true });
|
|
}
|
|
else if (param.type === "boolean") {
|
|
const provided = await promptBooleanParam(param, resolvedDefault);
|
|
return new ParamValue(provided.toString(), false, { boolean: true });
|
|
}
|
|
else if (param.type === "list") {
|
|
const provided = await promptList(param, projectId, resolvedDefault);
|
|
return ParamValue.fromList(provided, param.delimiter);
|
|
}
|
|
else if (param.type === "secret") {
|
|
throw new error_1.FirebaseError(`Somehow ended up trying to interactively prompt for secret parameter ${param.name}, which should never happen.`);
|
|
}
|
|
(0, functional_1.assertExhaustive)(param);
|
|
}
|
|
async function promptList(param, projectId, resolvedDefault) {
|
|
if (!param.input) {
|
|
const defaultToText = { text: {} };
|
|
param.input = defaultToText;
|
|
}
|
|
let prompt;
|
|
if (isSelectInput(param.input)) {
|
|
throw new error_1.FirebaseError("List params cannot have non-list selector inputs");
|
|
}
|
|
else if (isMultiSelectInput(param.input)) {
|
|
prompt = `Select a value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
return promptSelectMultiple(prompt, param.input, resolvedDefault, (res) => res);
|
|
}
|
|
else if (isTextInput(param.input)) {
|
|
prompt = `Enter a list of strings (delimiter: ${param.delimiter ? param.delimiter : ","}) for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
return promptText(prompt, param.input, resolvedDefault, (res) => {
|
|
return res.split(param.delimiter || ",");
|
|
});
|
|
}
|
|
else if (isResourceInput(param.input)) {
|
|
prompt = `Select values for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
return promptResourceStrings(prompt, param.input, projectId);
|
|
}
|
|
else {
|
|
(0, functional_1.assertExhaustive)(param.input);
|
|
}
|
|
}
|
|
async function promptBooleanParam(param, resolvedDefault) {
|
|
if (!param.input) {
|
|
const defaultToText = { text: {} };
|
|
param.input = defaultToText;
|
|
}
|
|
const isTruthyInput = (res) => ["true", "y", "yes", "1"].includes(res.toLowerCase());
|
|
let prompt;
|
|
if (isSelectInput(param.input)) {
|
|
prompt = `Select a value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
return promptSelect(prompt, param.input, resolvedDefault, isTruthyInput);
|
|
}
|
|
else if (isMultiSelectInput(param.input)) {
|
|
throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
|
|
}
|
|
else if (isTextInput(param.input)) {
|
|
prompt = `Enter a boolean value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
return promptText(prompt, param.input, resolvedDefault, isTruthyInput);
|
|
}
|
|
else if (isResourceInput(param.input)) {
|
|
throw new error_1.FirebaseError("Boolean params cannot have Cloud Resource selector inputs");
|
|
}
|
|
else {
|
|
(0, functional_1.assertExhaustive)(param.input);
|
|
}
|
|
}
|
|
async function promptStringParam(param, projectId, resolvedDefault) {
|
|
if (!param.input) {
|
|
const defaultToText = { text: {} };
|
|
param.input = defaultToText;
|
|
}
|
|
let prompt;
|
|
if (isResourceInput(param.input)) {
|
|
prompt = `Select a value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
return promptResourceString(prompt, param.input, projectId, resolvedDefault);
|
|
}
|
|
else if (isMultiSelectInput(param.input)) {
|
|
throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
|
|
}
|
|
else if (isSelectInput(param.input)) {
|
|
prompt = `Select a value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
return promptSelect(prompt, param.input, resolvedDefault, (res) => res);
|
|
}
|
|
else if (isTextInput(param.input)) {
|
|
prompt = `Enter a string value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
return promptText(prompt, param.input, resolvedDefault, (res) => res);
|
|
}
|
|
else {
|
|
(0, functional_1.assertExhaustive)(param.input);
|
|
}
|
|
}
|
|
async function promptIntParam(param, resolvedDefault) {
|
|
if (!param.input) {
|
|
const defaultToText = { text: {} };
|
|
param.input = defaultToText;
|
|
}
|
|
let prompt;
|
|
if (isSelectInput(param.input)) {
|
|
prompt = `Select a value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
return promptSelect(prompt, param.input, resolvedDefault, (res) => {
|
|
if (isNaN(+res)) {
|
|
return { message: `"${res}" could not be converted to a number.` };
|
|
}
|
|
if (res.includes(".")) {
|
|
return { message: `${res} is not an integer value.` };
|
|
}
|
|
return +res;
|
|
});
|
|
}
|
|
else if (isMultiSelectInput(param.input)) {
|
|
throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
|
|
}
|
|
else if (isTextInput(param.input)) {
|
|
prompt = `Enter an integer value for ${param.label || param.name}:`;
|
|
if (param.description) {
|
|
prompt += ` \n(${param.description})`;
|
|
}
|
|
return promptText(prompt, param.input, resolvedDefault, (res) => {
|
|
if (isNaN(+res)) {
|
|
return { message: `"${res}" could not be converted to a number.` };
|
|
}
|
|
if (res.includes(".")) {
|
|
return { message: `${res} is not an integer value.` };
|
|
}
|
|
return +res;
|
|
});
|
|
}
|
|
else if (isResourceInput(param.input)) {
|
|
throw new error_1.FirebaseError("Numeric params cannot have Cloud Resource selector inputs");
|
|
}
|
|
else {
|
|
(0, functional_1.assertExhaustive)(param.input);
|
|
}
|
|
}
|
|
async function promptResourceString(prompt, input, projectId, resolvedDefault) {
|
|
const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
|
|
switch (input.resource.type) {
|
|
case "storage.googleapis.com/Bucket":
|
|
const buckets = await (0, storage_1.listBuckets)(projectId);
|
|
if (buckets.length === 0) {
|
|
throw notFound;
|
|
}
|
|
const forgedInput = {
|
|
select: {
|
|
options: buckets.map((bucketName) => {
|
|
return { label: bucketName, value: bucketName };
|
|
}),
|
|
},
|
|
};
|
|
return promptSelect(prompt, forgedInput, resolvedDefault, (res) => res);
|
|
default:
|
|
logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
|
|
return promptText(prompt, { text: {} }, resolvedDefault, (res) => res);
|
|
}
|
|
}
|
|
async function promptResourceStrings(prompt, input, projectId) {
|
|
const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
|
|
switch (input.resource.type) {
|
|
case "storage.googleapis.com/Bucket":
|
|
const buckets = await (0, storage_1.listBuckets)(projectId);
|
|
if (buckets.length === 0) {
|
|
throw notFound;
|
|
}
|
|
const forgedInput = {
|
|
multiSelect: {
|
|
options: buckets.map((bucketName) => {
|
|
return { label: bucketName, value: bucketName };
|
|
}),
|
|
},
|
|
};
|
|
return promptSelectMultiple(prompt, forgedInput, undefined, (res) => res);
|
|
default:
|
|
logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
|
|
return promptText(prompt, { text: {} }, undefined, (res) => res.split(","));
|
|
}
|
|
}
|
|
function shouldRetry(obj) {
|
|
return typeof obj === "object" && obj.message !== undefined;
|
|
}
|
|
async function promptText(prompt, input, resolvedDefault, converter) {
|
|
const res = await (0, prompt_1.promptOnce)({
|
|
type: "input",
|
|
default: resolvedDefault,
|
|
message: prompt,
|
|
});
|
|
if (input.text.validationRegex) {
|
|
const userRe = new RegExp(input.text.validationRegex);
|
|
if (!userRe.test(res)) {
|
|
logger_1.logger.error(input.text.validationErrorMessage ||
|
|
`Input did not match provided validator ${userRe.toString()}, retrying...`);
|
|
return promptText(prompt, input, resolvedDefault, converter);
|
|
}
|
|
}
|
|
const converted = converter(res.toString());
|
|
if (shouldRetry(converted)) {
|
|
logger_1.logger.error(converted.message);
|
|
return promptText(prompt, input, resolvedDefault, converter);
|
|
}
|
|
return converted;
|
|
}
|
|
async function promptSelect(prompt, input, resolvedDefault, converter) {
|
|
const response = await (0, prompt_1.promptOnce)({
|
|
name: "input",
|
|
type: "list",
|
|
default: resolvedDefault,
|
|
message: prompt,
|
|
choices: input.select.options.map((option) => {
|
|
return {
|
|
checked: false,
|
|
name: option.label,
|
|
value: option.value.toString(),
|
|
};
|
|
}),
|
|
});
|
|
const converted = converter(response);
|
|
if (shouldRetry(converted)) {
|
|
logger_1.logger.error(converted.message);
|
|
return promptSelect(prompt, input, resolvedDefault, converter);
|
|
}
|
|
return converted;
|
|
}
|
|
async function promptSelectMultiple(prompt, input, resolvedDefault, converter) {
|
|
const response = await (0, prompt_1.promptOnce)({
|
|
name: "input",
|
|
type: "checkbox",
|
|
default: resolvedDefault,
|
|
message: prompt,
|
|
choices: input.multiSelect.options.map((option) => {
|
|
return {
|
|
checked: false,
|
|
name: option.label,
|
|
value: option.value.toString(),
|
|
};
|
|
}),
|
|
});
|
|
const converted = converter(response);
|
|
if (shouldRetry(converted)) {
|
|
logger_1.logger.error(converted.message);
|
|
return promptSelectMultiple(prompt, input, resolvedDefault, converter);
|
|
}
|
|
return converted;
|
|
}
|