firebase-tools 11.7.0 → 11.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auth.js +1 -1
- package/lib/deploy/functions/build.js +39 -25
- package/lib/deploy/functions/cache/applyHash.js +29 -0
- package/lib/deploy/functions/cache/hash.js +30 -0
- package/lib/deploy/functions/cel.js +249 -0
- package/lib/deploy/functions/functionsDeployHelper.js +12 -1
- package/lib/deploy/functions/params.js +259 -102
- package/lib/deploy/functions/prepare.js +34 -4
- package/lib/deploy/functions/prepareFunctionsUpload.js +13 -5
- package/lib/deploy/functions/release/fabricator.js +17 -1
- package/lib/deploy/functions/release/planner.js +17 -0
- package/lib/deploy/functions/runtimes/node/parseTriggers.js +9 -0
- package/lib/deploy/functions/services/index.js +11 -0
- package/lib/deploy/functions/services/remoteConfig.js +14 -0
- package/lib/emulator/extensionsEmulator.js +1 -0
- package/lib/emulator/functionsEmulator.js +18 -59
- package/lib/emulator/functionsRuntimeWorker.js +38 -7
- package/lib/emulator/storage/apis/firebase.js +139 -125
- package/lib/emulator/storage/apis/gcloud.js +102 -42
- package/lib/emulator/storage/files.js +25 -15
- package/lib/emulator/storage/metadata.js +86 -56
- package/lib/emulator/storage/rules/runtime.js +10 -2
- package/lib/emulator/storage/upload.js +45 -9
- package/lib/extensions/extensionsHelper.js +1 -1
- package/lib/functions/constants.js +14 -0
- package/lib/functions/events/v2.js +2 -1
- package/lib/functions/secrets.js +8 -1
- package/lib/gcp/cloudfunctions.js +15 -18
- package/lib/gcp/cloudfunctionsv2.js +15 -18
- package/lib/gcp/cloudscheduler.js +2 -1
- package/lib/gcp/secretManager.js +15 -1
- package/lib/gcp/storage.js +15 -1
- package/lib/previews.js +1 -1
- package/lib/track.js +1 -1
- package/npm-shrinkwrap.json +54 -30
- package/package.json +6 -5
- package/templates/init/storage/storage.rules +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveParams = exports.resolveBoolean = exports.resolveString = exports.resolveInt = void 0;
|
|
3
|
+
exports.resolveParams = exports.ParamValue = exports.isResourceInput = exports.isSelectInput = exports.isTextInput = exports.resolveBoolean = exports.resolveString = exports.resolveInt = void 0;
|
|
4
4
|
const logger_1 = require("../../logger");
|
|
5
5
|
const error_1 = require("../../error");
|
|
6
6
|
const prompt_1 = require("../../prompt");
|
|
7
7
|
const functional_1 = require("../../functional");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const secretManager = require("../../gcp/secretManager");
|
|
9
|
+
const storage_1 = require("../../gcp/storage");
|
|
10
|
+
const cel_1 = require("./cel");
|
|
11
11
|
function dependenciesCEL(expr) {
|
|
12
12
|
const deps = [];
|
|
13
13
|
const paramCapture = /{{ params\.(\w+) }}/g;
|
|
@@ -21,70 +21,76 @@ function resolveInt(from, paramValues) {
|
|
|
21
21
|
if (typeof from === "number") {
|
|
22
22
|
return from;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
if (!match) {
|
|
26
|
-
throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
|
|
27
|
-
}
|
|
28
|
-
const referencedParamValue = paramValues[match[1]];
|
|
29
|
-
if (typeof referencedParamValue !== "number") {
|
|
30
|
-
throw new error_1.FirebaseError("Referenced numeric parameter '" +
|
|
31
|
-
match +
|
|
32
|
-
"' resolved to non-number value " +
|
|
33
|
-
referencedParamValue);
|
|
34
|
-
}
|
|
35
|
-
return referencedParamValue;
|
|
24
|
+
return (0, cel_1.resolveExpression)("number", from, paramValues);
|
|
36
25
|
}
|
|
37
26
|
exports.resolveInt = resolveInt;
|
|
38
27
|
function resolveString(from, paramValues) {
|
|
39
|
-
if (!isCEL(from)) {
|
|
40
|
-
return from;
|
|
41
|
-
}
|
|
42
28
|
let output = from;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (typeof referencedParamValue !== "string") {
|
|
48
|
-
throw new error_1.FirebaseError("Referenced string parameter '" +
|
|
49
|
-
match[1] +
|
|
50
|
-
"' resolved to non-string value " +
|
|
51
|
-
referencedParamValue);
|
|
52
|
-
}
|
|
53
|
-
output = output.replace(`{{ params.${match[1]} }}`, referencedParamValue);
|
|
29
|
+
const celCapture = /{{ .+? }}/g;
|
|
30
|
+
const subExprs = from.match(celCapture);
|
|
31
|
+
if (!subExprs || subExprs.length === 0) {
|
|
32
|
+
return output;
|
|
54
33
|
}
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
for (const expr of subExprs) {
|
|
35
|
+
const resolved = (0, cel_1.resolveExpression)("string", expr, paramValues);
|
|
36
|
+
output = output.replace(expr, resolved);
|
|
57
37
|
}
|
|
58
38
|
return output;
|
|
59
39
|
}
|
|
60
40
|
exports.resolveString = resolveString;
|
|
61
41
|
function resolveBoolean(from, paramValues) {
|
|
62
|
-
if (typeof from === "
|
|
63
|
-
|
|
64
|
-
const referencedParamValue = paramValues[match[1]];
|
|
65
|
-
if (typeof referencedParamValue !== "boolean") {
|
|
66
|
-
throw new error_1.FirebaseError("Referenced boolean parameter '" +
|
|
67
|
-
match +
|
|
68
|
-
"' resolved to non-boolean value " +
|
|
69
|
-
referencedParamValue);
|
|
70
|
-
}
|
|
71
|
-
return referencedParamValue;
|
|
72
|
-
}
|
|
73
|
-
else if (typeof from === "string") {
|
|
74
|
-
throw new error_1.FirebaseError("CEL evaluation of expression '" + from + "' not yet supported");
|
|
42
|
+
if (typeof from === "boolean") {
|
|
43
|
+
return from;
|
|
75
44
|
}
|
|
76
|
-
return from;
|
|
45
|
+
return (0, cel_1.resolveExpression)("boolean", from, paramValues);
|
|
77
46
|
}
|
|
78
47
|
exports.resolveBoolean = resolveBoolean;
|
|
48
|
+
function isTextInput(input) {
|
|
49
|
+
return {}.hasOwnProperty.call(input, "text");
|
|
50
|
+
}
|
|
51
|
+
exports.isTextInput = isTextInput;
|
|
52
|
+
function isSelectInput(input) {
|
|
53
|
+
return {}.hasOwnProperty.call(input, "select");
|
|
54
|
+
}
|
|
55
|
+
exports.isSelectInput = isSelectInput;
|
|
56
|
+
function isResourceInput(input) {
|
|
57
|
+
return {}.hasOwnProperty.call(input, "resource");
|
|
58
|
+
}
|
|
59
|
+
exports.isResourceInput = isResourceInput;
|
|
60
|
+
class ParamValue {
|
|
61
|
+
constructor(rawValue, secret, types) {
|
|
62
|
+
this.rawValue = rawValue;
|
|
63
|
+
this.secret = secret;
|
|
64
|
+
this.legalString = types.string || false;
|
|
65
|
+
this.legalBoolean = types.boolean || false;
|
|
66
|
+
this.legalNumber = types.number || false;
|
|
67
|
+
}
|
|
68
|
+
toString() {
|
|
69
|
+
return this.rawValue;
|
|
70
|
+
}
|
|
71
|
+
asString() {
|
|
72
|
+
return this.rawValue;
|
|
73
|
+
}
|
|
74
|
+
asBoolean() {
|
|
75
|
+
return ["true", "y", "yes", "1"].includes(this.rawValue);
|
|
76
|
+
}
|
|
77
|
+
asNumber() {
|
|
78
|
+
return +this.rawValue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.ParamValue = ParamValue;
|
|
79
82
|
function resolveDefaultCEL(type, expr, currentEnv) {
|
|
80
83
|
const deps = dependenciesCEL(expr);
|
|
81
84
|
const allDepsFound = deps.every((dep) => !!currentEnv[dep]);
|
|
82
|
-
|
|
85
|
+
const dependsOnSecret = deps.some((dep) => currentEnv[dep].secret);
|
|
86
|
+
if (!allDepsFound || dependsOnSecret) {
|
|
83
87
|
throw new error_1.FirebaseError("Build specified parameter with un-resolvable default value " +
|
|
84
88
|
expr +
|
|
85
89
|
"; dependencies missing.");
|
|
86
90
|
}
|
|
87
91
|
switch (type) {
|
|
92
|
+
case "boolean":
|
|
93
|
+
return resolveBoolean(expr, currentEnv);
|
|
88
94
|
case "string":
|
|
89
95
|
return resolveString(expr, currentEnv);
|
|
90
96
|
case "int":
|
|
@@ -100,93 +106,244 @@ function canSatisfyParam(param, value) {
|
|
|
100
106
|
else if (param.type === "int") {
|
|
101
107
|
return typeof value === "number" && Number.isInteger(value);
|
|
102
108
|
}
|
|
109
|
+
else if (param.type === "boolean") {
|
|
110
|
+
return typeof value === "boolean";
|
|
111
|
+
}
|
|
112
|
+
else if (param.type === "secret") {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
103
115
|
(0, functional_1.assertExhaustive)(param);
|
|
104
116
|
}
|
|
105
|
-
async function resolveParams(params, projectId, userEnvs) {
|
|
117
|
+
async function resolveParams(params, projectId, userEnvs, nonInteractive) {
|
|
106
118
|
const paramValues = {};
|
|
107
|
-
const [
|
|
119
|
+
const [resolved, outstanding] = (0, functional_1.partition)(params, (param) => {
|
|
108
120
|
return {}.hasOwnProperty.call(userEnvs, param.name);
|
|
109
121
|
});
|
|
110
|
-
for (const param of
|
|
111
|
-
if (!canSatisfyParam(param, userEnvs[param.name])) {
|
|
112
|
-
throw new error_1.FirebaseError("Parameter " +
|
|
113
|
-
param.name +
|
|
114
|
-
" resolved to value from dotenv files " +
|
|
115
|
-
userEnvs[param.name] +
|
|
116
|
-
" of wrong type");
|
|
117
|
-
}
|
|
122
|
+
for (const param of resolved) {
|
|
118
123
|
paramValues[param.name] = userEnvs[param.name];
|
|
119
124
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const [needSecret, needPrompt] = (0, functional_1.partition)(outstanding, (param) => param.type === "secret");
|
|
126
|
+
for (const param of needSecret) {
|
|
127
|
+
await handleSecret(param, projectId);
|
|
128
|
+
}
|
|
129
|
+
if (nonInteractive && needPrompt.length > 0) {
|
|
130
|
+
const envNames = outstanding.map((p) => p.name).join(", ");
|
|
131
|
+
throw new error_1.FirebaseError(`In non-interactive mode but have no value for the following environment variables: ${envNames}\n` +
|
|
132
|
+
"To continue, either run `firebase deploy` with an interactive terminal, or add values to a dotenv file. " +
|
|
133
|
+
"For information regarding how to use dotenv files, see https://firebase.google.com/docs/functions/config-env");
|
|
134
|
+
}
|
|
135
|
+
for (const param of needPrompt) {
|
|
136
|
+
const promptable = param;
|
|
137
|
+
let paramDefault = promptable.default;
|
|
138
|
+
if (paramDefault && (0, cel_1.isCelExpression)(paramDefault)) {
|
|
123
139
|
paramDefault = resolveDefaultCEL(param.type, paramDefault, paramValues);
|
|
124
140
|
}
|
|
125
141
|
if (paramDefault && !canSatisfyParam(param, paramDefault)) {
|
|
126
142
|
throw new error_1.FirebaseError("Parameter " + param.name + " has default value " + paramDefault + " of wrong type");
|
|
127
143
|
}
|
|
128
|
-
paramValues[param.name] = await promptParam(param, paramDefault);
|
|
144
|
+
paramValues[param.name] = await promptParam(param, projectId, paramDefault);
|
|
129
145
|
}
|
|
130
146
|
return paramValues;
|
|
131
147
|
}
|
|
132
148
|
exports.resolveParams = resolveParams;
|
|
133
|
-
async function
|
|
149
|
+
async function handleSecret(secretParam, projectId) {
|
|
150
|
+
const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest");
|
|
151
|
+
if (!metadata.secret) {
|
|
152
|
+
throw new error_1.FirebaseError(`Your project currently doesn't have any secret named ${secretParam.name}. Create one by running firebase functions:secrets:set ${secretParam.name} command and try the deploy again.`);
|
|
153
|
+
}
|
|
154
|
+
else if (!metadata.secretVersion) {
|
|
155
|
+
throw new error_1.FirebaseError(`Cloud Secret Manager has no latest version of the secret defined by param ${secretParam.label || secretParam.name}`);
|
|
156
|
+
}
|
|
157
|
+
else if (metadata.secretVersion.state === "DESTROYED" ||
|
|
158
|
+
metadata.secretVersion.state === "DISABLED") {
|
|
159
|
+
throw new error_1.FirebaseError(`Cloud Secret Manager's latest version of secret '${secretParam.label || secretParam.name} is in illegal state ${metadata.secretVersion.state}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function promptParam(param, projectId, resolvedDefault) {
|
|
134
163
|
if (param.type === "string") {
|
|
135
|
-
|
|
164
|
+
const provided = await promptStringParam(param, projectId, resolvedDefault);
|
|
165
|
+
return new ParamValue(provided.toString(), false, { string: true });
|
|
136
166
|
}
|
|
137
167
|
else if (param.type === "int") {
|
|
138
|
-
|
|
168
|
+
const provided = await promptIntParam(param, resolvedDefault);
|
|
169
|
+
return new ParamValue(provided.toString(), false, { number: true });
|
|
170
|
+
}
|
|
171
|
+
else if (param.type === "boolean") {
|
|
172
|
+
const provided = await promptBooleanParam(param, resolvedDefault);
|
|
173
|
+
return new ParamValue(provided.toString(), false, { boolean: true });
|
|
174
|
+
}
|
|
175
|
+
else if (param.type === "secret") {
|
|
176
|
+
throw new error_1.FirebaseError(`Somehow ended up trying to interactively prompt for secret parameter ${param.name}, which should never happen.`);
|
|
139
177
|
}
|
|
140
178
|
(0, functional_1.assertExhaustive)(param);
|
|
141
179
|
}
|
|
142
|
-
async function
|
|
180
|
+
async function promptBooleanParam(param, resolvedDefault) {
|
|
143
181
|
if (!param.input) {
|
|
144
|
-
const defaultToText = {
|
|
182
|
+
const defaultToText = { text: {} };
|
|
145
183
|
param.input = defaultToText;
|
|
146
184
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
const isTruthyInput = (res) => ["true", "y", "yes", "1"].includes(res.toLowerCase());
|
|
186
|
+
let prompt;
|
|
187
|
+
if (isSelectInput(param.input)) {
|
|
188
|
+
prompt = `Select a value for ${param.label || param.name}:`;
|
|
189
|
+
if (param.description) {
|
|
190
|
+
prompt += ` \n(${param.description})`;
|
|
191
|
+
}
|
|
192
|
+
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
193
|
+
return promptSelect(prompt, param.input, resolvedDefault, isTruthyInput);
|
|
194
|
+
}
|
|
195
|
+
else if (isTextInput(param.input)) {
|
|
196
|
+
prompt = `Enter a boolean value for ${param.label || param.name}:`;
|
|
197
|
+
if (param.description) {
|
|
198
|
+
prompt += ` \n(${param.description})`;
|
|
199
|
+
}
|
|
200
|
+
return promptText(prompt, param.input, resolvedDefault, isTruthyInput);
|
|
201
|
+
}
|
|
202
|
+
else if (isResourceInput(param.input)) {
|
|
203
|
+
throw new error_1.FirebaseError("Boolean params cannot have Cloud Resource selector inputs");
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
(0, functional_1.assertExhaustive)(param.input);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function promptStringParam(param, projectId, resolvedDefault) {
|
|
210
|
+
if (!param.input) {
|
|
211
|
+
const defaultToText = { text: {} };
|
|
212
|
+
param.input = defaultToText;
|
|
213
|
+
}
|
|
214
|
+
let prompt;
|
|
215
|
+
if (isResourceInput(param.input)) {
|
|
216
|
+
prompt = `Select a value for ${param.label || param.name}:`;
|
|
217
|
+
if (param.description) {
|
|
218
|
+
prompt += ` \n(${param.description})`;
|
|
219
|
+
}
|
|
220
|
+
return promptResourceString(prompt, param.input, projectId, resolvedDefault);
|
|
221
|
+
}
|
|
222
|
+
else if (isSelectInput(param.input)) {
|
|
223
|
+
prompt = `Select a value for ${param.label || param.name}:`;
|
|
224
|
+
if (param.description) {
|
|
225
|
+
prompt += ` \n(${param.description})`;
|
|
226
|
+
}
|
|
227
|
+
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
228
|
+
return promptSelect(prompt, param.input, resolvedDefault, (res) => res);
|
|
229
|
+
}
|
|
230
|
+
else if (isTextInput(param.input)) {
|
|
231
|
+
prompt = `Enter a string value for ${param.label || param.name}:`;
|
|
232
|
+
if (param.description) {
|
|
233
|
+
prompt += ` \n(${param.description})`;
|
|
234
|
+
}
|
|
235
|
+
return promptText(prompt, param.input, resolvedDefault, (res) => res);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
(0, functional_1.assertExhaustive)(param.input);
|
|
162
239
|
}
|
|
163
240
|
}
|
|
164
241
|
async function promptIntParam(param, resolvedDefault) {
|
|
165
242
|
if (!param.input) {
|
|
166
|
-
const defaultToText = {
|
|
243
|
+
const defaultToText = { text: {} };
|
|
167
244
|
param.input = defaultToText;
|
|
168
245
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
246
|
+
let prompt;
|
|
247
|
+
if (isSelectInput(param.input)) {
|
|
248
|
+
prompt = `Select a value for ${param.label || param.name}:`;
|
|
249
|
+
if (param.description) {
|
|
250
|
+
prompt += ` \n(${param.description})`;
|
|
251
|
+
}
|
|
252
|
+
prompt += "\nSelect an option with the arrow keys, and use Enter to confirm your choice. ";
|
|
253
|
+
return promptSelect(prompt, param.input, resolvedDefault, (res) => {
|
|
254
|
+
if (isNaN(+res)) {
|
|
255
|
+
return { message: `"${res}" could not be converted to a number.` };
|
|
256
|
+
}
|
|
257
|
+
if (res.includes(".")) {
|
|
258
|
+
return { message: `${res} is not an integer value.` };
|
|
177
259
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
260
|
+
return +res;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (isTextInput(param.input)) {
|
|
264
|
+
prompt = `Enter an integer value for ${param.label || param.name}:`;
|
|
265
|
+
if (param.description) {
|
|
266
|
+
prompt += ` \n(${param.description})`;
|
|
267
|
+
}
|
|
268
|
+
return promptText(prompt, param.input, resolvedDefault, (res) => {
|
|
269
|
+
if (isNaN(+res)) {
|
|
270
|
+
return { message: `"${res}" could not be converted to a number.` };
|
|
271
|
+
}
|
|
272
|
+
if (res.includes(".")) {
|
|
273
|
+
return { message: `${res} is not an integer value.` };
|
|
190
274
|
}
|
|
275
|
+
return +res;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else if (isResourceInput(param.input)) {
|
|
279
|
+
throw new error_1.FirebaseError("Numeric params cannot have Cloud Resource selector inputs");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
(0, functional_1.assertExhaustive)(param.input);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function promptResourceString(prompt, input, projectId, resolvedDefault) {
|
|
286
|
+
const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
|
|
287
|
+
switch (input.resource.type) {
|
|
288
|
+
case "storage.googleapis.com/Bucket":
|
|
289
|
+
const buckets = await (0, storage_1.listBuckets)(projectId);
|
|
290
|
+
if (buckets.length === 0) {
|
|
291
|
+
throw notFound;
|
|
292
|
+
}
|
|
293
|
+
const forgedInput = {
|
|
294
|
+
select: {
|
|
295
|
+
options: buckets.map((bucketName) => {
|
|
296
|
+
return { label: bucketName, value: bucketName };
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
return promptSelect(prompt, forgedInput, resolvedDefault, (res) => res);
|
|
301
|
+
default:
|
|
302
|
+
logger_1.logger.warn(`Warning: unknown resource type ${input.resource.type}; defaulting to raw text input...`);
|
|
303
|
+
return promptText(prompt, { text: {} }, resolvedDefault, (res) => res);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function promptText(prompt, input, resolvedDefault, converter) {
|
|
307
|
+
const res = await (0, prompt_1.promptOnce)({
|
|
308
|
+
type: "input",
|
|
309
|
+
default: resolvedDefault,
|
|
310
|
+
message: prompt,
|
|
311
|
+
});
|
|
312
|
+
if (input.text.validationRegex) {
|
|
313
|
+
const userRe = new RegExp(input.text.validationRegex);
|
|
314
|
+
if (!userRe.test(res)) {
|
|
315
|
+
logger_1.logger.error(input.text.validationErrorMessage ||
|
|
316
|
+
`Input did not match provided validator ${userRe.toString()}, retrying...`);
|
|
317
|
+
return promptText(prompt, input, resolvedDefault, converter);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const converted = converter(res);
|
|
321
|
+
if (typeof converted === "object") {
|
|
322
|
+
logger_1.logger.error(converted.message);
|
|
323
|
+
return promptText(prompt, input, resolvedDefault, converter);
|
|
324
|
+
}
|
|
325
|
+
return converted;
|
|
326
|
+
}
|
|
327
|
+
async function promptSelect(prompt, input, resolvedDefault, converter) {
|
|
328
|
+
const response = await (0, prompt_1.promptOnce)({
|
|
329
|
+
name: "input",
|
|
330
|
+
type: "list",
|
|
331
|
+
default: () => {
|
|
332
|
+
resolvedDefault;
|
|
333
|
+
},
|
|
334
|
+
message: prompt,
|
|
335
|
+
choices: input.select.options.map((option) => {
|
|
336
|
+
return {
|
|
337
|
+
checked: false,
|
|
338
|
+
name: option.label,
|
|
339
|
+
value: option.value.toString(),
|
|
340
|
+
};
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
const converted = converter(response);
|
|
344
|
+
if (typeof converted === "object") {
|
|
345
|
+
logger_1.logger.error(converted.message);
|
|
346
|
+
return promptSelect(prompt, input, resolvedDefault, converter);
|
|
191
347
|
}
|
|
348
|
+
return converted;
|
|
192
349
|
}
|
|
@@ -23,10 +23,13 @@ const error_1 = require("../../error");
|
|
|
23
23
|
const projectConfig_1 = require("../../functions/projectConfig");
|
|
24
24
|
const v1_1 = require("../../functions/events/v1");
|
|
25
25
|
const serviceusage_1 = require("../../gcp/serviceusage");
|
|
26
|
+
const previews_1 = require("../../previews");
|
|
27
|
+
const applyHash_1 = require("./cache/applyHash");
|
|
26
28
|
function hasUserConfig(config) {
|
|
27
29
|
return Object.keys(config).length > 1;
|
|
28
30
|
}
|
|
29
31
|
async function prepare(context, options, payload) {
|
|
32
|
+
var _a;
|
|
30
33
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
31
34
|
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
32
35
|
context.config = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions);
|
|
@@ -78,16 +81,36 @@ async function prepare(context, options, payload) {
|
|
|
78
81
|
const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
|
|
79
82
|
const envs = Object.assign(Object.assign({}, userEnvs), firebaseEnvs);
|
|
80
83
|
const wantBuild = await runtimeDelegate.discoverBuild(runtimeConfig, firebaseEnvs);
|
|
81
|
-
const wantBackend = await build.resolveBackend(wantBuild, userEnvOpt, userEnvs);
|
|
84
|
+
const { backend: wantBackend, envs: resolvedEnvs } = await build.resolveBackend(wantBuild, userEnvOpt, userEnvs, options.nonInteractive);
|
|
85
|
+
let hasEnvsFromParams = false;
|
|
82
86
|
wantBackend.environmentVariables = envs;
|
|
87
|
+
for (const envName of Object.keys(resolvedEnvs)) {
|
|
88
|
+
const envValue = (_a = resolvedEnvs[envName]) === null || _a === void 0 ? void 0 : _a.toString();
|
|
89
|
+
if (envValue &&
|
|
90
|
+
!Object.prototype.hasOwnProperty.call(wantBackend.environmentVariables, envName)) {
|
|
91
|
+
wantBackend.environmentVariables[envName] = envValue;
|
|
92
|
+
hasEnvsFromParams = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
83
95
|
for (const endpoint of backend.allEndpoints(wantBackend)) {
|
|
84
96
|
endpoint.environmentVariables = wantBackend.environmentVariables;
|
|
85
97
|
endpoint.codebase = codebase;
|
|
86
98
|
}
|
|
87
99
|
wantBackends[codebase] = wantBackend;
|
|
88
|
-
if (functionsEnv.hasUserEnvs(userEnvOpt)) {
|
|
100
|
+
if (functionsEnv.hasUserEnvs(userEnvOpt) || hasEnvsFromParams) {
|
|
89
101
|
codebaseUsesEnvs.push(codebase);
|
|
90
102
|
}
|
|
103
|
+
if (wantBuild.params.length > 0) {
|
|
104
|
+
if (wantBuild.params.every((p) => p.type !== "secret")) {
|
|
105
|
+
void (0, track_1.track)("functions_params_in_build", "env_only");
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
void (0, track_1.track)("functions_params_in_build", "with_secrets");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
void (0, track_1.track)("functions_params_in_build", "none");
|
|
113
|
+
}
|
|
91
114
|
}
|
|
92
115
|
validate.endpointsAreUnique(wantBackends);
|
|
93
116
|
for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
|
|
@@ -99,10 +122,14 @@ async function prepare(context, options, payload) {
|
|
|
99
122
|
(0, utils_1.logLabeledBullet)("functions", `preparing ${clc.bold(sourceDirName)} directory for uploading...`);
|
|
100
123
|
}
|
|
101
124
|
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
|
|
102
|
-
|
|
125
|
+
const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config);
|
|
126
|
+
source.functionsSourceV2 = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.pathToSource;
|
|
127
|
+
source.functionsSourceV2Hash = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.hash;
|
|
103
128
|
}
|
|
104
129
|
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) {
|
|
105
|
-
|
|
130
|
+
const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config, runtimeConfig);
|
|
131
|
+
source.functionsSourceV1 = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.pathToSource;
|
|
132
|
+
source.functionsSourceV1Hash = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.hash;
|
|
106
133
|
}
|
|
107
134
|
context.sources[codebase] = source;
|
|
108
135
|
}
|
|
@@ -160,6 +187,9 @@ async function prepare(context, options, payload) {
|
|
|
160
187
|
await (0, checkIam_1.ensureServiceAgentRoles)(projectId, projectNumber, matchingBackend, haveBackend);
|
|
161
188
|
await validate.secretsAreValid(projectId, matchingBackend);
|
|
162
189
|
await ensure.secretAccess(projectId, matchingBackend, haveBackend);
|
|
190
|
+
if (previews_1.previews.skipdeployingnoopfunctions) {
|
|
191
|
+
(0, applyHash_1.applyBackendHashToBackends)(wantBackends, context);
|
|
192
|
+
}
|
|
163
193
|
}
|
|
164
194
|
exports.prepare = prepare;
|
|
165
195
|
function inferDetailsFromExisting(want, have, usedDotenv) {
|
|
@@ -9,6 +9,7 @@ const path = require("path");
|
|
|
9
9
|
const tmp = require("tmp");
|
|
10
10
|
const error_1 = require("../../error");
|
|
11
11
|
const logger_1 = require("../../logger");
|
|
12
|
+
const hash_1 = require("./cache/hash");
|
|
12
13
|
const functionsConfig = require("../../functionsConfig");
|
|
13
14
|
const utils = require("../../utils");
|
|
14
15
|
const fsAsync = require("../../fsAsync");
|
|
@@ -36,10 +37,11 @@ async function getFunctionsConfig(projectId) {
|
|
|
36
37
|
}
|
|
37
38
|
exports.getFunctionsConfig = getFunctionsConfig;
|
|
38
39
|
async function pipeAsync(from, to) {
|
|
40
|
+
from.pipe(to);
|
|
41
|
+
await from.finalize();
|
|
39
42
|
return new Promise((resolve, reject) => {
|
|
40
43
|
to.on("finish", resolve);
|
|
41
44
|
to.on("error", reject);
|
|
42
|
-
from.pipe(to);
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
async function packageSource(sourceDir, config, runtimeConfig) {
|
|
@@ -49,23 +51,28 @@ async function packageSource(sourceDir, config, runtimeConfig) {
|
|
|
49
51
|
encoding: "binary",
|
|
50
52
|
});
|
|
51
53
|
const archive = archiver("zip");
|
|
54
|
+
const hashes = [];
|
|
52
55
|
const ignore = config.ignore || ["node_modules", ".git"];
|
|
53
56
|
ignore.push("firebase-debug.log", "firebase-debug.*.log", CONFIG_DEST_FILE);
|
|
54
57
|
try {
|
|
55
58
|
const files = await fsAsync.readdirRecursive({ path: sourceDir, ignore: ignore });
|
|
56
59
|
for (const file of files) {
|
|
60
|
+
const name = path.relative(sourceDir, file.name);
|
|
61
|
+
const fileHash = await (0, hash_1.getSourceHash)(file.name);
|
|
62
|
+
hashes.push(fileHash);
|
|
57
63
|
archive.file(file.name, {
|
|
58
|
-
name
|
|
64
|
+
name,
|
|
59
65
|
mode: file.mode,
|
|
60
66
|
});
|
|
61
67
|
}
|
|
62
68
|
if (typeof runtimeConfig !== "undefined") {
|
|
63
|
-
|
|
69
|
+
const runtimeConfigString = JSON.stringify(runtimeConfig, null, 2);
|
|
70
|
+
hashes.push(runtimeConfigString);
|
|
71
|
+
archive.append(runtimeConfigString, {
|
|
64
72
|
name: CONFIG_DEST_FILE,
|
|
65
73
|
mode: 420,
|
|
66
74
|
});
|
|
67
75
|
}
|
|
68
|
-
archive.finalize();
|
|
69
76
|
await pipeAsync(archive, fileStream);
|
|
70
77
|
}
|
|
71
78
|
catch (err) {
|
|
@@ -80,7 +87,8 @@ async function packageSource(sourceDir, config, runtimeConfig) {
|
|
|
80
87
|
" (" +
|
|
81
88
|
filesize(archive.pointer()) +
|
|
82
89
|
") for uploading");
|
|
83
|
-
|
|
90
|
+
const hash = hashes.join(".");
|
|
91
|
+
return { pathToSource: tmpFile, hash };
|
|
84
92
|
}
|
|
85
93
|
async function prepareFunctionsUpload(sourceDir, config, runtimeConfig) {
|
|
86
94
|
return packageSource(sourceDir, config, runtimeConfig);
|
|
@@ -91,6 +91,9 @@ class Fabricator {
|
|
|
91
91
|
this.logOpStart("creating", endpoint);
|
|
92
92
|
upserts.push(handle("create", endpoint, () => this.createEndpoint(endpoint, scraper)));
|
|
93
93
|
}
|
|
94
|
+
for (const endpoint of changes.endpointsToSkip) {
|
|
95
|
+
utils.logSuccess(this.getLogSuccessMessage("skip", endpoint));
|
|
96
|
+
}
|
|
94
97
|
for (const update of changes.endpointsToUpdate) {
|
|
95
98
|
this.logOpStart("updating", update.endpoint);
|
|
96
99
|
upserts.push(handle("update", update.endpoint, () => this.updateEndpoint(update, scraper)));
|
|
@@ -526,8 +529,21 @@ class Fabricator {
|
|
|
526
529
|
utils.logLabeledBullet("functions", `${op} ${runtime} function ${clc.bold(label)}...`);
|
|
527
530
|
}
|
|
528
531
|
logOpSuccess(op, endpoint) {
|
|
532
|
+
utils.logSuccess(this.getLogSuccessMessage(op, endpoint));
|
|
533
|
+
}
|
|
534
|
+
getLogSuccessMessage(op, endpoint) {
|
|
529
535
|
const label = helper.getFunctionLabel(endpoint);
|
|
530
|
-
|
|
536
|
+
switch (op) {
|
|
537
|
+
case "skip":
|
|
538
|
+
return `${clc.bold(clc.magenta(`functions[${label}]`))} Skipped (No changes detected)`;
|
|
539
|
+
default:
|
|
540
|
+
return `${clc.bold(clc.green(`functions[${label}]`))} Successful ${op} operation.`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
getSkippedDeployingNopOpMessage(endpoints) {
|
|
544
|
+
const functionNames = endpoints.map((endpoint) => endpoint.id).join(",");
|
|
545
|
+
return `${clc.bold(clc.magenta(`functions:`))} You can re-deploy skipped functions with:
|
|
546
|
+
${clc.bold(`firebase deploy --only functions:${functionNames}`)} or ${clc.bold(`FUNCTIONS_DEPLOY_UNCHANGED=true firebase deploy`)}`;
|
|
531
547
|
}
|
|
532
548
|
}
|
|
533
549
|
exports.Fabricator = Fabricator;
|
|
@@ -7,6 +7,7 @@ const error_1 = require("../../../error");
|
|
|
7
7
|
const utils = require("../../../utils");
|
|
8
8
|
const backend = require("../backend");
|
|
9
9
|
const v2events = require("../../../functions/events/v2");
|
|
10
|
+
const previews_1 = require("../../../previews");
|
|
10
11
|
function calculateChangesets(want, have, keyFn, deleteAll) {
|
|
11
12
|
const toCreate = utils.groupBy(Object.keys(want)
|
|
12
13
|
.filter((id) => !have[id])
|
|
@@ -15,20 +16,36 @@ function calculateChangesets(want, have, keyFn, deleteAll) {
|
|
|
15
16
|
.filter((id) => !want[id])
|
|
16
17
|
.filter((id) => deleteAll || (0, deploymentTool_1.isFirebaseManaged)(have[id].labels || {}))
|
|
17
18
|
.map((id) => have[id]), keyFn);
|
|
19
|
+
const { skipdeployingnoopfunctions } = previews_1.previews;
|
|
20
|
+
const toSkipPredicate = (id) => !!(skipdeployingnoopfunctions &&
|
|
21
|
+
have[id].hash &&
|
|
22
|
+
want[id].hash &&
|
|
23
|
+
want[id].hash === have[id].hash);
|
|
24
|
+
const toSkipEndpointsMap = Object.keys(want)
|
|
25
|
+
.filter((id) => have[id])
|
|
26
|
+
.filter((id) => toSkipPredicate(id))
|
|
27
|
+
.reduce((memo, id) => {
|
|
28
|
+
memo[id] = want[id];
|
|
29
|
+
return memo;
|
|
30
|
+
}, {});
|
|
31
|
+
const toSkip = utils.groupBy(Object.values(toSkipEndpointsMap), keyFn);
|
|
18
32
|
const toUpdate = utils.groupBy(Object.keys(want)
|
|
19
33
|
.filter((id) => have[id])
|
|
34
|
+
.filter((id) => !toSkipEndpointsMap[id])
|
|
20
35
|
.map((id) => calculateUpdate(want[id], have[id])), (eu) => keyFn(eu.endpoint));
|
|
21
36
|
const result = {};
|
|
22
37
|
const keys = new Set([
|
|
23
38
|
...Object.keys(toCreate),
|
|
24
39
|
...Object.keys(toDelete),
|
|
25
40
|
...Object.keys(toUpdate),
|
|
41
|
+
...Object.keys(toSkip),
|
|
26
42
|
]);
|
|
27
43
|
for (const key of keys) {
|
|
28
44
|
result[key] = {
|
|
29
45
|
endpointsToCreate: toCreate[key] || [],
|
|
30
46
|
endpointsToUpdate: toUpdate[key] || [],
|
|
31
47
|
endpointsToDelete: toDelete[key] || [],
|
|
48
|
+
endpointsToSkip: toSkip[key] || [],
|
|
32
49
|
};
|
|
33
50
|
}
|
|
34
51
|
return result;
|