No Description
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.

params.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.resolveParams = exports.ParamValue = exports.isMultiSelectInput = exports.isResourceInput = exports.isSelectInput = exports.isTextInput = exports.resolveBoolean = exports.resolveList = exports.resolveString = exports.resolveInt = void 0;
  4. const logger_1 = require("../../logger");
  5. const error_1 = require("../../error");
  6. const prompt_1 = require("../../prompt");
  7. const functional_1 = require("../../functional");
  8. const secretManager = require("../../gcp/secretManager");
  9. const storage_1 = require("../../gcp/storage");
  10. const cel_1 = require("./cel");
  11. function dependenciesCEL(expr) {
  12. const deps = [];
  13. const paramCapture = /{{ params\.(\w+) }}/g;
  14. let match;
  15. while ((match = paramCapture.exec(expr)) != null) {
  16. deps.push(match[1]);
  17. }
  18. return deps;
  19. }
  20. function resolveInt(from, paramValues) {
  21. if (typeof from === "number") {
  22. return from;
  23. }
  24. return (0, cel_1.resolveExpression)("number", from, paramValues);
  25. }
  26. exports.resolveInt = resolveInt;
  27. function resolveString(from, paramValues) {
  28. let output = from;
  29. const celCapture = /{{ .+? }}/g;
  30. const subExprs = from.match(celCapture);
  31. if (!subExprs || subExprs.length === 0) {
  32. return output;
  33. }
  34. for (const expr of subExprs) {
  35. const resolved = (0, cel_1.resolveExpression)("string", expr, paramValues);
  36. output = output.replace(expr, resolved);
  37. }
  38. return output;
  39. }
  40. exports.resolveString = resolveString;
  41. function resolveList(from, paramValues) {
  42. if (!from) {
  43. return [];
  44. }
  45. else if (Array.isArray(from)) {
  46. return from.map((entry) => resolveString(entry, paramValues));
  47. }
  48. else if (typeof from === "string") {
  49. return (0, cel_1.resolveExpression)("string[]", from, paramValues);
  50. }
  51. else {
  52. (0, functional_1.assertExhaustive)(from);
  53. }
  54. }
  55. exports.resolveList = resolveList;
  56. function resolveBoolean(from, paramValues) {
  57. if (typeof from === "boolean") {
  58. return from;
  59. }
  60. return (0, cel_1.resolveExpression)("boolean", from, paramValues);
  61. }
  62. exports.resolveBoolean = resolveBoolean;
  63. function isTextInput(input) {
  64. return {}.hasOwnProperty.call(input, "text");
  65. }
  66. exports.isTextInput = isTextInput;
  67. function isSelectInput(input) {
  68. return {}.hasOwnProperty.call(input, "select");
  69. }
  70. exports.isSelectInput = isSelectInput;
  71. function isResourceInput(input) {
  72. return {}.hasOwnProperty.call(input, "resource");
  73. }
  74. exports.isResourceInput = isResourceInput;
  75. function isMultiSelectInput(input) {
  76. return {}.hasOwnProperty.call(input, "multiSelect");
  77. }
  78. exports.isMultiSelectInput = isMultiSelectInput;
  79. class ParamValue {
  80. constructor(rawValue, internal, types) {
  81. this.rawValue = rawValue;
  82. this.internal = internal;
  83. this.legalString = types.string || false;
  84. this.legalBoolean = types.boolean || false;
  85. this.legalNumber = types.number || false;
  86. this.legalList = types.list || false;
  87. this.delimiter = ",";
  88. }
  89. static fromList(ls, delimiter = ",") {
  90. const pv = new ParamValue(ls.join(delimiter), false, { list: true });
  91. pv.setDelimiter(delimiter);
  92. return pv;
  93. }
  94. setDelimiter(delimiter) {
  95. this.delimiter = delimiter;
  96. }
  97. toString() {
  98. return this.rawValue;
  99. }
  100. toSDK() {
  101. return this.legalList ? JSON.stringify(this.asList()) : this.toString();
  102. }
  103. asString() {
  104. return this.rawValue;
  105. }
  106. asBoolean() {
  107. return ["true", "y", "yes", "1"].includes(this.rawValue);
  108. }
  109. asList() {
  110. return this.rawValue.split(this.delimiter);
  111. }
  112. asNumber() {
  113. return +this.rawValue;
  114. }
  115. }
  116. exports.ParamValue = ParamValue;
  117. function resolveDefaultCEL(type, expr, currentEnv) {
  118. const deps = dependenciesCEL(expr);
  119. const allDepsFound = deps.every((dep) => !!currentEnv[dep]);
  120. if (!allDepsFound) {
  121. throw new error_1.FirebaseError("Build specified parameter with un-resolvable default value " +
  122. expr +
  123. "; dependencies missing.");
  124. }
  125. switch (type) {
  126. case "boolean":
  127. return resolveBoolean(expr, currentEnv);
  128. case "string":
  129. return resolveString(expr, currentEnv);
  130. case "int":
  131. return resolveInt(expr, currentEnv);
  132. case "list":
  133. return resolveList(expr, currentEnv);
  134. default:
  135. throw new error_1.FirebaseError("Build specified parameter with default " + expr + " of unsupported type");
  136. }
  137. }
  138. function canSatisfyParam(param, value) {
  139. if (param.type === "string") {
  140. return typeof value === "string";
  141. }
  142. else if (param.type === "int") {
  143. return typeof value === "number" && Number.isInteger(value);
  144. }
  145. else if (param.type === "boolean") {
  146. return typeof value === "boolean";
  147. }
  148. else if (param.type === "list") {
  149. return Array.isArray(value);
  150. }
  151. else if (param.type === "secret") {
  152. return false;
  153. }
  154. (0, functional_1.assertExhaustive)(param);
  155. }
  156. async function resolveParams(params, firebaseConfig, userEnvs, nonInteractive) {
  157. const paramValues = populateDefaultParams(firebaseConfig);
  158. const [resolved, outstanding] = (0, functional_1.partition)(params, (param) => {
  159. return {}.hasOwnProperty.call(userEnvs, param.name);
  160. });
  161. for (const param of resolved) {
  162. paramValues[param.name] = userEnvs[param.name];
  163. }
  164. const [needSecret, needPrompt] = (0, functional_1.partition)(outstanding, (param) => param.type === "secret");
  165. for (const param of needSecret) {
  166. await handleSecret(param, firebaseConfig.projectId);
  167. }
  168. if (nonInteractive && needPrompt.length > 0) {
  169. const envNames = outstanding.map((p) => p.name).join(", ");
  170. throw new error_1.FirebaseError(`In non-interactive mode but have no value for the following environment variables: ${envNames}\n` +
  171. "To continue, either run `firebase deploy` with an interactive terminal, or add values to a dotenv file. " +
  172. "For information regarding how to use dotenv files, see https://firebase.google.com/docs/functions/config-env");
  173. }
  174. for (const param of needPrompt) {
  175. const promptable = param;
  176. let paramDefault = promptable.default;
  177. if (paramDefault && (0, cel_1.isCelExpression)(paramDefault)) {
  178. paramDefault = resolveDefaultCEL(param.type, paramDefault, paramValues);
  179. }
  180. if (paramDefault && !canSatisfyParam(param, paramDefault)) {
  181. throw new error_1.FirebaseError("Parameter " + param.name + " has default value " + paramDefault + " of wrong type");
  182. }
  183. paramValues[param.name] = await promptParam(param, firebaseConfig.projectId, paramDefault);
  184. }
  185. return paramValues;
  186. }
  187. exports.resolveParams = resolveParams;
  188. function populateDefaultParams(config) {
  189. const defaultParams = {};
  190. if (config.databaseURL && config.databaseURL !== "") {
  191. defaultParams["DATABASE_URL"] = new ParamValue(config.databaseURL, true, {
  192. string: true,
  193. boolean: false,
  194. number: false,
  195. });
  196. }
  197. defaultParams["PROJECT_ID"] = new ParamValue(config.projectId, true, {
  198. string: true,
  199. boolean: false,
  200. number: false,
  201. });
  202. defaultParams["GCLOUD_PROJECT"] = new ParamValue(config.projectId, true, {
  203. string: true,
  204. boolean: false,
  205. number: false,
  206. });
  207. if (config.storageBucket && config.storageBucket !== "") {
  208. defaultParams["STORAGE_BUCKET"] = new ParamValue(config.storageBucket, true, {
  209. string: true,
  210. boolean: false,
  211. number: false,
  212. });
  213. }
  214. return defaultParams;
  215. }
  216. async function handleSecret(secretParam, projectId) {
  217. const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest");
  218. if (!metadata.secret) {
  219. const secretValue = await (0, prompt_1.promptOnce)({
  220. name: secretParam.name,
  221. type: "password",
  222. 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}:`,
  223. });
  224. const secretLabel = { "firebase-hosting-managed": "yes" };
  225. await secretManager.createSecret(projectId, secretParam.name, secretLabel);
  226. await secretManager.addVersion(projectId, secretParam.name, secretValue);
  227. return secretValue;
  228. }
  229. else if (!metadata.secretVersion) {
  230. throw new error_1.FirebaseError(`Cloud Secret Manager has no latest version of the secret defined by param ${secretParam.label || secretParam.name}`);
  231. }
  232. else if (metadata.secretVersion.state === "DESTROYED" ||
  233. metadata.secretVersion.state === "DISABLED") {
  234. throw new error_1.FirebaseError(`Cloud Secret Manager's latest version of secret '${secretParam.label || secretParam.name} is in illegal state ${metadata.secretVersion.state}`);
  235. }
  236. }
  237. async function promptParam(param, projectId, resolvedDefault) {
  238. if (param.type === "string") {
  239. const provided = await promptStringParam(param, projectId, resolvedDefault);
  240. return new ParamValue(provided.toString(), false, { string: true });
  241. }
  242. else if (param.type === "int") {
  243. const provided = await promptIntParam(param, resolvedDefault);
  244. return new ParamValue(provided.toString(), false, { number: true });
  245. }
  246. else if (param.type === "boolean") {
  247. const provided = await promptBooleanParam(param, resolvedDefault);
  248. return new ParamValue(provided.toString(), false, { boolean: true });
  249. }
  250. else if (param.type === "list") {
  251. const provided = await promptList(param, projectId, resolvedDefault);
  252. return ParamValue.fromList(provided, param.delimiter);
  253. }
  254. else if (param.type === "secret") {
  255. throw new error_1.FirebaseError(`Somehow ended up trying to interactively prompt for secret parameter ${param.name}, which should never happen.`);
  256. }
  257. (0, functional_1.assertExhaustive)(param);
  258. }
  259. async function promptList(param, projectId, resolvedDefault) {
  260. if (!param.input) {
  261. const defaultToText = { text: {} };
  262. param.input = defaultToText;
  263. }
  264. let prompt;
  265. if (isSelectInput(param.input)) {
  266. throw new error_1.FirebaseError("List params cannot have non-list selector inputs");
  267. }
  268. else if (isMultiSelectInput(param.input)) {
  269. prompt = `Select a value for ${param.label || param.name}:`;
  270. if (param.description) {
  271. prompt += ` \n(${param.description})`;
  272. }
  273. prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
  274. return promptSelectMultiple(prompt, param.input, resolvedDefault, (res) => res);
  275. }
  276. else if (isTextInput(param.input)) {
  277. prompt = `Enter a list of strings (delimiter: ${param.delimiter ? param.delimiter : ","}) for ${param.label || param.name}:`;
  278. if (param.description) {
  279. prompt += ` \n(${param.description})`;
  280. }
  281. return promptText(prompt, param.input, resolvedDefault, (res) => {
  282. return res.split(param.delimiter || ",");
  283. });
  284. }
  285. else if (isResourceInput(param.input)) {
  286. prompt = `Select values for ${param.label || param.name}:`;
  287. if (param.description) {
  288. prompt += ` \n(${param.description})`;
  289. }
  290. return promptResourceStrings(prompt, param.input, projectId);
  291. }
  292. else {
  293. (0, functional_1.assertExhaustive)(param.input);
  294. }
  295. }
  296. async function promptBooleanParam(param, resolvedDefault) {
  297. if (!param.input) {
  298. const defaultToText = { text: {} };
  299. param.input = defaultToText;
  300. }
  301. const isTruthyInput = (res) => ["true", "y", "yes", "1"].includes(res.toLowerCase());
  302. let prompt;
  303. if (isSelectInput(param.input)) {
  304. prompt = `Select a value for ${param.label || param.name}:`;
  305. if (param.description) {
  306. prompt += ` \n(${param.description})`;
  307. }
  308. prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
  309. return promptSelect(prompt, param.input, resolvedDefault, isTruthyInput);
  310. }
  311. else if (isMultiSelectInput(param.input)) {
  312. throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
  313. }
  314. else if (isTextInput(param.input)) {
  315. prompt = `Enter a boolean value for ${param.label || param.name}:`;
  316. if (param.description) {
  317. prompt += ` \n(${param.description})`;
  318. }
  319. return promptText(prompt, param.input, resolvedDefault, isTruthyInput);
  320. }
  321. else if (isResourceInput(param.input)) {
  322. throw new error_1.FirebaseError("Boolean params cannot have Cloud Resource selector inputs");
  323. }
  324. else {
  325. (0, functional_1.assertExhaustive)(param.input);
  326. }
  327. }
  328. async function promptStringParam(param, projectId, resolvedDefault) {
  329. if (!param.input) {
  330. const defaultToText = { text: {} };
  331. param.input = defaultToText;
  332. }
  333. let prompt;
  334. if (isResourceInput(param.input)) {
  335. prompt = `Select a value for ${param.label || param.name}:`;
  336. if (param.description) {
  337. prompt += ` \n(${param.description})`;
  338. }
  339. return promptResourceString(prompt, param.input, projectId, resolvedDefault);
  340. }
  341. else if (isMultiSelectInput(param.input)) {
  342. throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
  343. }
  344. else if (isSelectInput(param.input)) {
  345. prompt = `Select a value for ${param.label || param.name}:`;
  346. if (param.description) {
  347. prompt += ` \n(${param.description})`;
  348. }
  349. prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
  350. return promptSelect(prompt, param.input, resolvedDefault, (res) => res);
  351. }
  352. else if (isTextInput(param.input)) {
  353. prompt = `Enter a string value for ${param.label || param.name}:`;
  354. if (param.description) {
  355. prompt += ` \n(${param.description})`;
  356. }
  357. return promptText(prompt, param.input, resolvedDefault, (res) => res);
  358. }
  359. else {
  360. (0, functional_1.assertExhaustive)(param.input);
  361. }
  362. }
  363. async function promptIntParam(param, resolvedDefault) {
  364. if (!param.input) {
  365. const defaultToText = { text: {} };
  366. param.input = defaultToText;
  367. }
  368. let prompt;
  369. if (isSelectInput(param.input)) {
  370. prompt = `Select a value for ${param.label || param.name}:`;
  371. if (param.description) {
  372. prompt += ` \n(${param.description})`;
  373. }
  374. prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
  375. return promptSelect(prompt, param.input, resolvedDefault, (res) => {
  376. if (isNaN(+res)) {
  377. return { message: `"${res}" could not be converted to a number.` };
  378. }
  379. if (res.includes(".")) {
  380. return { message: `${res} is not an integer value.` };
  381. }
  382. return +res;
  383. });
  384. }
  385. else if (isMultiSelectInput(param.input)) {
  386. throw new error_1.FirebaseError("Non-list params cannot have multi selector inputs");
  387. }
  388. else if (isTextInput(param.input)) {
  389. prompt = `Enter an integer value for ${param.label || param.name}:`;
  390. if (param.description) {
  391. prompt += ` \n(${param.description})`;
  392. }
  393. return promptText(prompt, param.input, resolvedDefault, (res) => {
  394. if (isNaN(+res)) {
  395. return { message: `"${res}" could not be converted to a number.` };
  396. }
  397. if (res.includes(".")) {
  398. return { message: `${res} is not an integer value.` };
  399. }
  400. return +res;
  401. });
  402. }
  403. else if (isResourceInput(param.input)) {
  404. throw new error_1.FirebaseError("Numeric params cannot have Cloud Resource selector inputs");
  405. }
  406. else {
  407. (0, functional_1.assertExhaustive)(param.input);
  408. }
  409. }
  410. async function promptResourceString(prompt, input, projectId, resolvedDefault) {
  411. const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
  412. switch (input.resource.type) {
  413. case "storage.googleapis.com/Bucket":
  414. const buckets = await (0, storage_1.listBuckets)(projectId);
  415. if (buckets.length === 0) {
  416. throw notFound;
  417. }
  418. const forgedInput = {
  419. select: {
  420. options: buckets.map((bucketName) => {
  421. return { label: bucketName, value: bucketName };
  422. }),
  423. },
  424. };
  425. return promptSelect(prompt, forgedInput, resolvedDefault, (res) => res);
  426. default:
  427. logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
  428. return promptText(prompt, { text: {} }, resolvedDefault, (res) => res);
  429. }
  430. }
  431. async function promptResourceStrings(prompt, input, projectId) {
  432. const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
  433. switch (input.resource.type) {
  434. case "storage.googleapis.com/Bucket":
  435. const buckets = await (0, storage_1.listBuckets)(projectId);
  436. if (buckets.length === 0) {
  437. throw notFound;
  438. }
  439. const forgedInput = {
  440. multiSelect: {
  441. options: buckets.map((bucketName) => {
  442. return { label: bucketName, value: bucketName };
  443. }),
  444. },
  445. };
  446. return promptSelectMultiple(prompt, forgedInput, undefined, (res) => res);
  447. default:
  448. logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
  449. return promptText(prompt, { text: {} }, undefined, (res) => res.split(","));
  450. }
  451. }
  452. function shouldRetry(obj) {
  453. return typeof obj === "object" && obj.message !== undefined;
  454. }
  455. async function promptText(prompt, input, resolvedDefault, converter) {
  456. const res = await (0, prompt_1.promptOnce)({
  457. type: "input",
  458. default: resolvedDefault,
  459. message: prompt,
  460. });
  461. if (input.text.validationRegex) {
  462. const userRe = new RegExp(input.text.validationRegex);
  463. if (!userRe.test(res)) {
  464. logger_1.logger.error(input.text.validationErrorMessage ||
  465. `Input did not match provided validator ${userRe.toString()}, retrying...`);
  466. return promptText(prompt, input, resolvedDefault, converter);
  467. }
  468. }
  469. const converted = converter(res.toString());
  470. if (shouldRetry(converted)) {
  471. logger_1.logger.error(converted.message);
  472. return promptText(prompt, input, resolvedDefault, converter);
  473. }
  474. return converted;
  475. }
  476. async function promptSelect(prompt, input, resolvedDefault, converter) {
  477. const response = await (0, prompt_1.promptOnce)({
  478. name: "input",
  479. type: "list",
  480. default: resolvedDefault,
  481. message: prompt,
  482. choices: input.select.options.map((option) => {
  483. return {
  484. checked: false,
  485. name: option.label,
  486. value: option.value.toString(),
  487. };
  488. }),
  489. });
  490. const converted = converter(response);
  491. if (shouldRetry(converted)) {
  492. logger_1.logger.error(converted.message);
  493. return promptSelect(prompt, input, resolvedDefault, converter);
  494. }
  495. return converted;
  496. }
  497. async function promptSelectMultiple(prompt, input, resolvedDefault, converter) {
  498. const response = await (0, prompt_1.promptOnce)({
  499. name: "input",
  500. type: "checkbox",
  501. default: resolvedDefault,
  502. message: prompt,
  503. choices: input.multiSelect.options.map((option) => {
  504. return {
  505. checked: false,
  506. name: option.label,
  507. value: option.value.toString(),
  508. };
  509. }),
  510. });
  511. const converted = converter(response);
  512. if (shouldRetry(converted)) {
  513. logger_1.logger.error(converted.message);
  514. return promptSelectMultiple(prompt, input, resolvedDefault, converter);
  515. }
  516. return converted;
  517. }