firebase-tools 9.20.0 → 9.21.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/lib/apiv2.js +4 -2
  3. package/lib/commands/crashlytics-symbols-upload.js +1 -1
  4. package/lib/commands/ext-dev-unpublish.js +10 -3
  5. package/lib/commands/functions-delete.js +53 -42
  6. package/lib/commands/functions-list.js +11 -11
  7. package/lib/deploy/functions/backend.js +77 -115
  8. package/lib/deploy/functions/checkIam.js +8 -8
  9. package/lib/deploy/functions/deploy.js +4 -10
  10. package/lib/deploy/functions/functionsDeployHelper.js +3 -68
  11. package/lib/deploy/functions/prepare.js +61 -29
  12. package/lib/deploy/functions/pricing.js +17 -17
  13. package/lib/deploy/functions/prompts.js +22 -21
  14. package/lib/deploy/functions/release/executor.js +39 -0
  15. package/lib/deploy/functions/release/fabricator.js +362 -0
  16. package/lib/deploy/functions/release/index.js +69 -0
  17. package/lib/deploy/functions/release/planner.js +159 -0
  18. package/lib/deploy/functions/release/reporter.js +162 -0
  19. package/lib/deploy/functions/release/sourceTokenScraper.js +28 -0
  20. package/lib/deploy/functions/release/timer.js +14 -0
  21. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +102 -127
  22. package/lib/deploy/functions/runtimes/node/parseTriggers.js +22 -43
  23. package/lib/deploy/functions/triggerRegionHelper.js +28 -20
  24. package/lib/deploy/functions/validate.js +1 -24
  25. package/lib/emulator/auth/apiSpec.js +37 -6
  26. package/lib/emulator/auth/operations.js +18 -8
  27. package/lib/emulator/auth/server.js +16 -2
  28. package/lib/emulator/auth/state.js +34 -15
  29. package/lib/emulator/downloadableEmulators.js +7 -7
  30. package/lib/emulator/functionsEmulator.js +15 -3
  31. package/lib/emulator/storage/cloudFunctions.js +37 -7
  32. package/lib/extensions/extensionsHelper.js +1 -1
  33. package/lib/gcp/cloudfunctions.js +1 -68
  34. package/lib/gcp/cloudfunctionsv2.js +2 -94
  35. package/lib/gcp/cloudscheduler.js +22 -16
  36. package/lib/gcp/pubsub.js +1 -9
  37. package/lib/utils.js +30 -1
  38. package/package.json +1 -1
  39. package/lib/deploy/functions/deploymentPlanner.js +0 -113
  40. package/lib/deploy/functions/deploymentTimer.js +0 -23
  41. package/lib/deploy/functions/errorHandler.js +0 -75
  42. package/lib/deploy/functions/release.js +0 -116
  43. package/lib/deploy/functions/tasks.js +0 -324
  44. package/lib/functions/listFunctions.js +0 -10
  45. package/lib/functionsDelete.js +0 -60
package/CHANGELOG.md CHANGED
@@ -1 +1,3 @@
1
- - `ext:install`, `ext:update` and `ext:configure` now support param type `secret`.
1
+ - Fix Auth Emulator deleteTenant not working with Node Admin (#3817).
2
+ - Fix Crashlytics Android Native Symbols not working on Windows due to ":" in the path (#3842)
3
+ - Fixes Firestore emulator UI showing requests out of order
package/lib/apiv2.js CHANGED
@@ -118,8 +118,10 @@ class Client {
118
118
  reqOptions.headers.set("User-Agent", `FirebaseCLI/${CLI_VERSION}`);
119
119
  }
120
120
  reqOptions.headers.set("X-Client-Version", `FirebaseCLI/${CLI_VERSION}`);
121
- if (reqOptions.responseType === "json") {
122
- reqOptions.headers.set("Content-Type", "application/json");
121
+ if (!reqOptions.headers.has("Content-Type")) {
122
+ if (reqOptions.responseType === "json") {
123
+ reqOptions.headers.set("Content-Type", "application/json");
124
+ }
123
125
  }
124
126
  return reqOptions;
125
127
  }
@@ -40,7 +40,7 @@ exports.default = new command_1.Command("crashlytics:symbols:upload <symbolFiles
40
40
  jarFile,
41
41
  app,
42
42
  generator,
43
- cachePath: path.join(SYMBOL_CACHE_ROOT_DIR, `crashlytics-${uuid.v4()}`, "nativeSymbols", app, generator),
43
+ cachePath: path.join(SYMBOL_CACHE_ROOT_DIR, `crashlytics-${uuid.v4()}`, "nativeSymbols", app.replace(/:/g, "-"), generator),
44
44
  symbolFile: "",
45
45
  generate: true,
46
46
  };
@@ -12,11 +12,12 @@ const error_1 = require("../error");
12
12
  const checkMinRequiredVersion_1 = require("../checkMinRequiredVersion");
13
13
  module.exports = new command_1.Command("ext:dev:unpublish <extensionRef>")
14
14
  .description("unpublish an extension")
15
+ .withForce()
15
16
  .help("use this command to unpublish an extension, and make it unavailable for developers to install or reconfigure. " +
16
17
  "Specify the extension you want to unpublish using the format '<publisherId>/<extensionId>.")
17
18
  .before(requireAuth_1.requireAuth)
18
19
  .before(checkMinRequiredVersion_1.checkMinRequiredVersion, "extDevMinVersion")
19
- .action(async (extensionRef) => {
20
+ .action(async (extensionRef, options) => {
20
21
  const { publisherId, extensionId, version } = refs.parse(extensionRef);
21
22
  utils.logLabeledWarning(extensionsHelper_1.logPrefix, "If you unpublish this extension, developers won't be able to install it. For developers who currently have this extension installed, it will continue to run and will appear as unpublished when listed in the Firebase console or Firebase CLI.");
22
23
  utils.logLabeledWarning("This is a permanent action", `Once unpublished, you may never use the extension name '${clc.bold(extensionId)}' again.`);
@@ -24,14 +25,20 @@ module.exports = new command_1.Command("ext:dev:unpublish <extensionRef>")
24
25
  throw new error_1.FirebaseError(`Unpublishing a single version is not currently supported. You can only unpublish ${clc.bold("ALL versions")} of an extension. To unpublish all versions, please remove the version from the reference.`);
25
26
  }
26
27
  await extensionsApi_1.getExtension(extensionRef);
27
- const consent = await comfirmUnpublish(publisherId, extensionId);
28
+ const consent = await comfirmUnpublish(publisherId, extensionId, options);
28
29
  if (!consent) {
29
30
  throw new error_1.FirebaseError("unpublishing cancelled.");
30
31
  }
31
32
  await extensionsApi_1.unpublishExtension(extensionRef);
32
33
  utils.logLabeledSuccess(extensionsHelper_1.logPrefix, "successfully unpublished all versions of this extension.");
33
34
  });
34
- async function comfirmUnpublish(publisherId, extensionId) {
35
+ async function comfirmUnpublish(publisherId, extensionId, options) {
36
+ if (options.nonInteractive && !options.force) {
37
+ throw new error_1.FirebaseError("Pass the --force flag to use this command in non-interactive mode");
38
+ }
39
+ if (options.nonInteractive && options.force) {
40
+ return true;
41
+ }
35
42
  const message = `You are about to unpublish ALL versions of ${clc.green(`${publisherId}/${extensionId}`)}.\nDo you wish to continue? `;
36
43
  return prompt_1.promptOnce({
37
44
  type: "confirm",
@@ -1,16 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const command_1 = require("../command");
4
3
  const clc = require("cli-color");
5
4
  const functionsConfig = require("../functionsConfig");
6
- const functionsDelete_1 = require("../functionsDelete");
5
+ const command_1 = require("../command");
6
+ const error_1 = require("../error");
7
7
  const projectUtils_1 = require("../projectUtils");
8
8
  const prompt_1 = require("../prompt");
9
- const helper = require("../deploy/functions/functionsDeployHelper");
9
+ const functional_1 = require("../functional");
10
10
  const requirePermissions_1 = require("../requirePermissions");
11
+ const helper = require("../deploy/functions/functionsDeployHelper");
11
12
  const utils = require("../utils");
12
13
  const backend = require("../deploy/functions/backend");
13
- const error_1 = require("../error");
14
+ const planner = require("../deploy/functions/release/planner");
15
+ const fabricator = require("../deploy/functions/release/fabricator");
16
+ const executor = require("../deploy/functions/release/executor");
17
+ const reporter = require("../deploy/functions/release/reporter");
14
18
  exports.default = new command_1.Command("functions:delete [filters...]")
15
19
  .description("delete one or more Cloud Functions by name or group name.")
16
20
  .option("--region <region>", "Specify region of the function to be deleted. " +
@@ -23,48 +27,55 @@ exports.default = new command_1.Command("functions:delete [filters...]")
23
27
  }
24
28
  const context = {
25
29
  projectId: projectUtils_1.needProjectId(options),
30
+ filters: filters.map((f) => f.split(".")),
26
31
  };
27
- const filterChunks = filters.map((filter) => {
28
- return filter.split(".");
32
+ const [config, existingBackend] = await Promise.all([
33
+ functionsConfig.getFirebaseConfig(options),
34
+ backend.existingBackend(context),
35
+ ]);
36
+ await backend.checkAvailability(context, backend.empty());
37
+ const appEngineLocation = functionsConfig.getAppEngineLocation(config);
38
+ if (options.region) {
39
+ existingBackend.endpoints = { [options.region]: existingBackend.endpoints[options.region] };
40
+ }
41
+ const plan = planner.createDeploymentPlan(backend.empty(), existingBackend, {
42
+ filters: context.filters,
43
+ deleteAll: true,
44
+ });
45
+ const allEpToDelete = Object.values(plan)
46
+ .map((changes) => changes.endpointsToDelete)
47
+ .reduce(functional_1.reduceFlat, [])
48
+ .sort(backend.compareFunctions);
49
+ if (allEpToDelete.length === 0) {
50
+ throw new error_1.FirebaseError(`The specified filters do not match any existing functions in project ${clc.bold(context.projectId)}.`);
51
+ }
52
+ const deleteList = allEpToDelete.map((func) => `\t${helper.getFunctionLabel(func)}`).join("\n");
53
+ const confirmDeletion = await prompt_1.promptOnce({
54
+ type: "confirm",
55
+ name: "force",
56
+ default: false,
57
+ message: "You are about to delete the following Cloud Functions:\n" +
58
+ deleteList +
59
+ "\n Are you sure?",
60
+ }, options);
61
+ if (!confirmDeletion) {
62
+ throw new error_1.FirebaseError("Command aborted.");
63
+ }
64
+ const functionExecutor = new executor.QueueExecutor({
65
+ retries: 30,
66
+ backoff: 20000,
67
+ concurrency: 40,
68
+ maxBackoff: 40000,
29
69
  });
30
70
  try {
31
- const [config, existingBackend] = await Promise.all([
32
- functionsConfig.getFirebaseConfig(options),
33
- backend.existingBackend(context),
34
- ]);
35
- await backend.checkAvailability(context, backend.empty());
36
- const appEngineLocation = functionsConfig.getAppEngineLocation(config);
37
- const functionsToDelete = existingBackend.cloudFunctions.filter((fn) => {
38
- const regionMatches = options.region ? fn.region === options.region : true;
39
- const nameMatches = helper.functionMatchesAnyGroup(fn, filterChunks);
40
- return regionMatches && nameMatches;
41
- });
42
- if (functionsToDelete.length === 0) {
43
- throw new Error(`The specified filters do not match any existing functions in project ${clc.bold(context.projectId)}.`);
44
- }
45
- const schedulesToDelete = existingBackend.schedules.filter((schedule) => {
46
- functionsToDelete.some(backend.sameFunctionName(schedule.targetService));
47
- });
48
- const topicsToDelete = existingBackend.topics.filter((topic) => {
49
- functionsToDelete.some(backend.sameFunctionName(topic.targetService));
71
+ const fab = new fabricator.Fabricator({
72
+ functionExecutor,
73
+ executor: new executor.QueueExecutor({}),
74
+ appEngineLocation,
50
75
  });
51
- const deleteList = functionsToDelete
52
- .map((func) => {
53
- return "\t" + helper.getFunctionLabel(func);
54
- })
55
- .join("\n");
56
- const confirmDeletion = await prompt_1.promptOnce({
57
- type: "confirm",
58
- name: "force",
59
- default: false,
60
- message: "You are about to delete the following Cloud Functions:\n" +
61
- deleteList +
62
- "\n Are you sure?",
63
- }, options);
64
- if (!confirmDeletion) {
65
- throw new Error("Command aborted.");
66
- }
67
- return await functionsDelete_1.deleteFunctions(functionsToDelete, schedulesToDelete, topicsToDelete, appEngineLocation);
76
+ const summary = await fab.applyPlan(plan);
77
+ await reporter.logAndTrackDeployStats(summary);
78
+ reporter.printErrors(summary);
68
79
  }
69
80
  catch (err) {
70
81
  throw new error_1.FirebaseError("Failed to delete functions", {
@@ -5,7 +5,6 @@ const error_1 = require("../error");
5
5
  const projectUtils_1 = require("../projectUtils");
6
6
  const requirePermissions_1 = require("../requirePermissions");
7
7
  const backend = require("../deploy/functions/backend");
8
- const listFunctions_1 = require("../functions/listFunctions");
9
8
  const previews_1 = require("../previews");
10
9
  const logger_1 = require("../logger");
11
10
  const Table = require("cli-table");
@@ -17,7 +16,8 @@ exports.default = new command_1.Command("functions:list")
17
16
  const context = {
18
17
  projectId: projectUtils_1.needProjectId(options),
19
18
  };
20
- const functionList = await listFunctions_1.listFunctions(context);
19
+ const existing = await backend.existingBackend(context);
20
+ const endpointsList = backend.allEndpoints(existing).sort(backend.compareFunctions);
21
21
  const table = previews_1.previews.functionsv2
22
22
  ? new Table({
23
23
  head: ["Function", "Version", "Trigger", "Location", "Memory", "Runtime"],
@@ -27,23 +27,23 @@ exports.default = new command_1.Command("functions:list")
27
27
  head: ["Function", "Trigger", "Location", "Memory", "Runtime"],
28
28
  style: { head: ["yellow"] },
29
29
  });
30
- for (const fnSpec of functionList.functions) {
31
- const trigger = backend.isEventTrigger(fnSpec.trigger) ? fnSpec.trigger.eventType : "https";
32
- const availableMemoryMb = fnSpec.availableMemoryMb || "---";
30
+ for (const endpoint of endpointsList) {
31
+ const trigger = backend.endpointTriggerType(endpoint);
32
+ const availableMemoryMb = endpoint.availableMemoryMb || "---";
33
33
  const entry = previews_1.previews.functionsv2
34
34
  ? [
35
- fnSpec.entryPoint,
36
- fnSpec.platform === "gcfv2" ? "v2" : "v1",
35
+ endpoint.id,
36
+ endpoint.platform === "gcfv2" ? "v2" : "v1",
37
37
  trigger,
38
- fnSpec.region,
38
+ endpoint.region,
39
39
  availableMemoryMb,
40
- fnSpec.runtime,
40
+ endpoint.runtime,
41
41
  ]
42
- : [fnSpec.entryPoint, trigger, fnSpec.region, availableMemoryMb, fnSpec.runtime];
42
+ : [endpoint.id, trigger, endpoint.region, availableMemoryMb, endpoint.runtime];
43
43
  table.push(entry);
44
44
  }
45
45
  logger_1.logger.info(table.toString());
46
- return functionList;
46
+ return endpointsList;
47
47
  }
48
48
  catch (err) {
49
49
  throw new error_1.FirebaseError("Failed to list functions", {
@@ -1,38 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.compareFunctions = exports.checkAvailability = exports.existingBackend = exports.scheduleIdForFunction = exports.topicName = exports.scheduleName = exports.sameFunctionName = exports.functionName = exports.isEmptyBackend = exports.empty = exports.isScheduleTriggered = exports.isEventTriggered = exports.isHttpsTriggered = exports.SCHEDULED_FUNCTION_LABEL = exports.memoryOptionDisplayName = exports.triggerTag = exports.isEventTrigger = void 0;
3
+ exports.compareFunctions = exports.missingEndpoint = exports.hasEndpoint = exports.regionalEndpoints = exports.matchingBackend = exports.someEndpoint = exports.allEndpoints = exports.checkAvailability = exports.existingBackend = exports.scheduleIdForFunction = exports.functionName = exports.isEmptyBackend = exports.of = exports.empty = exports.isScheduleTriggered = exports.isEventTriggered = exports.isHttpsTriggered = exports.SCHEDULED_FUNCTION_LABEL = exports.memoryOptionDisplayName = exports.endpointTriggerType = void 0;
4
4
  const gcf = require("../../gcp/cloudfunctions");
5
5
  const gcfV2 = require("../../gcp/cloudfunctionsv2");
6
6
  const utils = require("../../utils");
7
7
  const error_1 = require("../../error");
8
8
  const previews_1 = require("../../previews");
9
- function isEventTrigger(trigger) {
10
- return "eventType" in trigger;
11
- }
12
- exports.isEventTrigger = isEventTrigger;
13
- function triggerTag(fn) {
14
- var _a, _b;
15
- if ((_a = fn.labels) === null || _a === void 0 ? void 0 : _a["deployment-scheduled"]) {
16
- if (fn.platform === "gcfv1") {
17
- return "v1.scheduled";
18
- }
19
- return "v2.scheduled";
9
+ function endpointTriggerType(endpoint) {
10
+ if (isScheduleTriggered(endpoint)) {
11
+ return "scheduled";
20
12
  }
21
- if ((_b = fn.labels) === null || _b === void 0 ? void 0 : _b["deployment-callable"]) {
22
- if (fn.platform === "gcfv1") {
23
- return "v1.callable";
24
- }
25
- return "v2.callable";
13
+ else if (isHttpsTriggered(endpoint)) {
14
+ return "https";
26
15
  }
27
- if (!isEventTrigger(fn.trigger)) {
28
- if (fn.platform === "gcfv1") {
29
- return "v1.https";
30
- }
31
- return "v2.https";
16
+ else if (isEventTriggered(endpoint)) {
17
+ return endpoint.eventTrigger.eventType;
18
+ }
19
+ else {
20
+ throw new Error("Unexpected trigger type for endpoint " + JSON.stringify(endpoint));
32
21
  }
33
- return fn.trigger.eventType;
34
22
  }
35
- exports.triggerTag = triggerTag;
23
+ exports.endpointTriggerType = endpointTriggerType;
36
24
  function memoryOptionDisplayName(option) {
37
25
  return {
38
26
  128: "128MB",
@@ -61,36 +49,31 @@ exports.isScheduleTriggered = isScheduleTriggered;
61
49
  function empty() {
62
50
  return {
63
51
  requiredAPIs: {},
64
- endpoints: [],
65
- cloudFunctions: [],
66
- schedules: [],
67
- topics: [],
52
+ endpoints: {},
68
53
  environmentVariables: {},
69
54
  };
70
55
  }
71
56
  exports.empty = empty;
57
+ function of(...endpoints) {
58
+ const bkend = Object.assign({}, empty());
59
+ for (const endpoint of endpoints) {
60
+ bkend.endpoints[endpoint.region] = bkend.endpoints[endpoint.region] || {};
61
+ if (bkend.endpoints[endpoint.region][endpoint.id]) {
62
+ throw new Error("Trying to create a backend with the same endpiont twice");
63
+ }
64
+ bkend.endpoints[endpoint.region][endpoint.id] = endpoint;
65
+ }
66
+ return bkend;
67
+ }
68
+ exports.of = of;
72
69
  function isEmptyBackend(backend) {
73
- return (Object.keys(backend.requiredAPIs).length == 0 &&
74
- backend.cloudFunctions.length === 0 &&
75
- backend.schedules.length === 0 &&
76
- backend.topics.length === 0);
70
+ return (Object.keys(backend.requiredAPIs).length == 0 && Object.keys(backend.endpoints).length === 0);
77
71
  }
78
72
  exports.isEmptyBackend = isEmptyBackend;
79
73
  function functionName(cloudFunction) {
80
74
  return `projects/${cloudFunction.project}/locations/${cloudFunction.region}/functions/${cloudFunction.id}`;
81
75
  }
82
76
  exports.functionName = functionName;
83
- exports.sameFunctionName = (func) => (test) => {
84
- return func.id === test.id && func.region === test.region && func.project == test.project;
85
- };
86
- function scheduleName(schedule, appEngineLocation) {
87
- return `projects/${schedule.project}/locations/${appEngineLocation}/jobs/${schedule.id}`;
88
- }
89
- exports.scheduleName = scheduleName;
90
- function topicName(topic) {
91
- return `projects/${topic.project}/topics/${topic.id}`;
92
- }
93
- exports.topicName = topicName;
94
77
  function scheduleIdForFunction(cloudFunction) {
95
78
  return `firebase-schedule-${cloudFunction.id}-${cloudFunction.region}`;
96
79
  }
@@ -104,7 +87,7 @@ async function existingBackend(context, forceRefresh) {
104
87
  }
105
88
  exports.existingBackend = existingBackend;
106
89
  async function loadExistingBackend(ctx) {
107
- var _a, _b, _c, _d;
90
+ var _a;
108
91
  ctx.loadedExistingBackend = true;
109
92
  ctx.existingBackend = Object.assign({}, empty());
110
93
  ctx.unreachableRegions = {
@@ -113,32 +96,10 @@ async function loadExistingBackend(ctx) {
113
96
  };
114
97
  const gcfV1Results = await gcf.listAllFunctions(ctx.projectId);
115
98
  for (const apiFunction of gcfV1Results.functions) {
116
- const specFunction = gcf.specFromFunction(apiFunction);
117
- ctx.existingBackend.cloudFunctions.push(specFunction);
118
- const isScheduled = ((_a = apiFunction.labels) === null || _a === void 0 ? void 0 : _a["deployment-scheduled"]) === "true";
119
- if (isScheduled) {
120
- const id = scheduleIdForFunction(specFunction);
121
- ctx.existingBackend.schedules.push({
122
- id,
123
- project: specFunction.project,
124
- transport: "pubsub",
125
- targetService: {
126
- id: specFunction.id,
127
- region: specFunction.region,
128
- project: specFunction.project,
129
- },
130
- });
131
- ctx.existingBackend.topics.push({
132
- id,
133
- project: specFunction.project,
134
- labels: exports.SCHEDULED_FUNCTION_LABEL,
135
- targetService: {
136
- id: specFunction.id,
137
- region: specFunction.region,
138
- project: specFunction.project,
139
- },
140
- });
141
- }
99
+ const endpoint = gcf.endpointFromFunction(apiFunction);
100
+ ctx.existingBackend.endpoints[endpoint.region] =
101
+ ctx.existingBackend.endpoints[endpoint.region] || {};
102
+ ctx.existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
142
103
  }
143
104
  ctx.unreachableRegions.gcfV1 = gcfV1Results.unreachable;
144
105
  if (!previews_1.previews.functionsv2) {
@@ -149,52 +110,16 @@ async function loadExistingBackend(ctx) {
149
110
  gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
150
111
  }
151
112
  catch (err) {
152
- if (err.status === 404 && ((_b = err.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes("method not found"))) {
113
+ if (err.status === 404 && ((_a = err.message) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes("method not found"))) {
153
114
  return;
154
115
  }
155
116
  throw err;
156
117
  }
157
118
  for (const apiFunction of gcfV2Results.functions) {
158
- const specFunction = gcfV2.specFromFunction(apiFunction);
159
- ctx.existingBackend.cloudFunctions.push(specFunction);
160
- const pubsubScheduled = ((_c = apiFunction.labels) === null || _c === void 0 ? void 0 : _c["deployment-scheduled"]) === "true";
161
- const httpsScheduled = ((_d = apiFunction.labels) === null || _d === void 0 ? void 0 : _d["deployment-scheduled"]) === "https";
162
- if (pubsubScheduled) {
163
- const id = scheduleIdForFunction(specFunction);
164
- ctx.existingBackend.schedules.push({
165
- id,
166
- project: specFunction.project,
167
- transport: "pubsub",
168
- targetService: {
169
- id: specFunction.id,
170
- region: specFunction.region,
171
- project: specFunction.project,
172
- },
173
- });
174
- ctx.existingBackend.topics.push({
175
- id,
176
- project: specFunction.project,
177
- labels: exports.SCHEDULED_FUNCTION_LABEL,
178
- targetService: {
179
- id: specFunction.id,
180
- region: specFunction.region,
181
- project: specFunction.project,
182
- },
183
- });
184
- }
185
- if (httpsScheduled) {
186
- const id = scheduleIdForFunction(specFunction);
187
- ctx.existingBackend.schedules.push({
188
- id,
189
- project: specFunction.project,
190
- transport: "https",
191
- targetService: {
192
- id: specFunction.id,
193
- region: specFunction.region,
194
- project: specFunction.project,
195
- },
196
- });
197
- }
119
+ const endpoint = gcfV2.endpointFromFunction(apiFunction);
120
+ ctx.existingBackend.endpoints[endpoint.region] =
121
+ ctx.existingBackend.endpoints[endpoint.region] || {};
122
+ ctx.existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
198
123
  }
199
124
  ctx.unreachableRegions.gcfV2 = gcfV2Results.unreachable;
200
125
  }
@@ -205,12 +130,12 @@ async function checkAvailability(context, want) {
205
130
  }
206
131
  const gcfV1Regions = new Set();
207
132
  const gcfV2Regions = new Set();
208
- for (const fn of want.cloudFunctions) {
209
- if (fn.platform == "gcfv1") {
210
- gcfV1Regions.add(fn.region);
133
+ for (const ep of allEndpoints(want)) {
134
+ if (ep.platform == "gcfv1") {
135
+ gcfV1Regions.add(ep.region);
211
136
  }
212
137
  else {
213
- gcfV2Regions.add(fn.region);
138
+ gcfV2Regions.add(ep.region);
214
139
  }
215
140
  }
216
141
  const neededUnreachableV1 = ctx.unreachableRegions.gcfV1.filter((region) => gcfV1Regions.has(region));
@@ -237,6 +162,43 @@ async function checkAvailability(context, want) {
237
162
  }
238
163
  }
239
164
  exports.checkAvailability = checkAvailability;
165
+ function allEndpoints(backend) {
166
+ return Object.values(backend.endpoints).reduce((accum, perRegion) => {
167
+ return [...accum, ...Object.values(perRegion)];
168
+ }, []);
169
+ }
170
+ exports.allEndpoints = allEndpoints;
171
+ function someEndpoint(backend, predicate) {
172
+ for (const endpoints of Object.values(backend.endpoints)) {
173
+ if (Object.values(endpoints).some(predicate)) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+ exports.someEndpoint = someEndpoint;
180
+ function matchingBackend(backend, predicate) {
181
+ const filtered = Object.assign({}, empty());
182
+ for (const endpoint of allEndpoints(backend)) {
183
+ if (!predicate(endpoint)) {
184
+ continue;
185
+ }
186
+ filtered.endpoints[endpoint.region] = filtered.endpoints[endpoint.region] || {};
187
+ filtered.endpoints[endpoint.region][endpoint.id] = endpoint;
188
+ }
189
+ return filtered;
190
+ }
191
+ exports.matchingBackend = matchingBackend;
192
+ function regionalEndpoints(backend, region) {
193
+ return backend.endpoints[region] ? Object.values(backend.endpoints[region]) : [];
194
+ }
195
+ exports.regionalEndpoints = regionalEndpoints;
196
+ exports.hasEndpoint = (backend) => (endpoint) => {
197
+ return !!backend.endpoints[endpoint.region] && !!backend.endpoints[endpoint.region][endpoint.id];
198
+ };
199
+ exports.missingEndpoint = (backend) => (endpoint) => {
200
+ return !exports.hasEndpoint(backend)(endpoint);
201
+ };
240
202
  function compareFunctions(left, right) {
241
203
  if (left.platform != right.platform) {
242
204
  return right.platform < left.platform ? -1 : 1;
@@ -28,17 +28,17 @@ async function checkServiceAccountIam(projectId) {
28
28
  }
29
29
  exports.checkServiceAccountIam = checkServiceAccountIam;
30
30
  async function checkHttpIam(context, options, payload) {
31
- const functions = payload.functions.backend.cloudFunctions;
32
31
  const filterGroups = context.filters || functionsDeployHelper_1.getFilterGroups(options);
33
- const httpFunctions = functions
34
- .filter((f) => !backend.isEventTrigger(f.trigger))
32
+ const httpEndpoints = backend
33
+ .allEndpoints(payload.functions.backend)
34
+ .filter(backend.isHttpsTriggered)
35
35
  .filter((f) => functionsDeployHelper_1.functionMatchesAnyGroup(f, filterGroups));
36
- const existingFunctions = (await backend.existingBackend(context)).cloudFunctions;
37
- const newHttpFunctions = httpFunctions.filter((func) => !existingFunctions.find(backend.sameFunctionName(func)));
38
- if (newHttpFunctions.length === 0) {
36
+ const existing = await backend.existingBackend(context);
37
+ const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
38
+ if (newHttpsEndpoints.length === 0) {
39
39
  return;
40
40
  }
41
- logger_1.logger.debug("[functions] found", newHttpFunctions.length, "new HTTP functions, testing setIamPolicy permission...");
41
+ logger_1.logger.debug("[functions] found", newHttpsEndpoints.length, "new HTTP functions, testing setIamPolicy permission...");
42
42
  let passed = true;
43
43
  try {
44
44
  const iamResult = await iam_1.testIamPermissions(context.projectId, [PERMISSION]);
@@ -51,7 +51,7 @@ async function checkHttpIam(context, options, payload) {
51
51
  if (!passed) {
52
52
  track("Error (User)", "deploy:functions:http_create_missing_iam");
53
53
  throw new error_1.FirebaseError(`Missing required permission on project ${cli_color_1.bold(context.projectId)} to deploy new HTTPS functions. The permission ${cli_color_1.bold(PERMISSION)} is required to deploy the following functions:\n\n- ` +
54
- newHttpFunctions.map((func) => func.id).join("\n- ") +
54
+ newHttpsEndpoints.map((func) => func.id).join("\n- ") +
55
55
  `\n\nTo address this error, please ask a project Owner to assign your account the "Cloud Functions Admin" role at the following URL:\n\nhttps://console.cloud.google.com/iam-admin/iam?project=${context.projectId}`);
56
56
  }
57
57
  logger_1.logger.debug("[functions] found setIamPolicy permission, proceeding with deploy");
@@ -11,6 +11,7 @@ const gcs = require("../../gcp/storage");
11
11
  const gcf = require("../../gcp/cloudfunctions");
12
12
  const gcfv2 = require("../../gcp/cloudfunctionsv2");
13
13
  const utils = require("../../utils");
14
+ const backend = require("./backend");
14
15
  const GCP_REGION = api_1.functionsUploadRegion;
15
16
  tmp_1.setGracefulCleanup();
16
17
  async function uploadSourceV1(context) {
@@ -44,18 +45,11 @@ async function deploy(context, options, payload) {
44
45
  try {
45
46
  const want = payload.functions.backend;
46
47
  const uploads = [];
47
- if (want.cloudFunctions.some((fn) => fn.platform === "gcfv1")) {
48
+ if (backend.allEndpoints(want).some((endpoint) => endpoint.platform === "gcfv1")) {
48
49
  uploads.push(uploadSourceV1(context));
49
50
  }
50
- if (want.cloudFunctions.some((fn) => fn.platform === "gcfv2")) {
51
- const functions = payload.functions.backend.cloudFunctions;
52
- const regions = [];
53
- for (const func of functions) {
54
- if (func.platform === "gcfv2" && -1 === regions.indexOf(func.region)) {
55
- regions.push(func.region);
56
- }
57
- }
58
- for (const region of regions) {
51
+ for (const region of Object.keys(want.endpoints)) {
52
+ if (backend.regionalEndpoints(want, region).some((e) => e.platform === "gcfv2")) {
59
53
  uploads.push(uploadSourceV2(context, region));
60
54
  }
61
55
  }
@@ -1,11 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.printTriggerUrls = exports.printSuccess = exports.logAndTrackDeployStats = exports.getFunctionLabel = exports.getFunctionId = exports.getFilterGroups = exports.functionMatchesGroup = exports.functionMatchesAnyGroup = void 0;
4
- const clc = require("cli-color");
5
- const logger_1 = require("../../logger");
6
- const backend = require("./backend");
7
- const track = require("../../track");
8
- const utils = require("../../utils");
3
+ exports.getFunctionLabel = exports.getFilterGroups = exports.functionMatchesGroup = exports.functionMatchesAnyGroup = void 0;
9
4
  function functionMatchesAnyGroup(func, filterGroups) {
10
5
  if (!filterGroups.length) {
11
6
  return true;
@@ -40,67 +35,7 @@ function getFilterGroups(options) {
40
35
  });
41
36
  }
42
37
  exports.getFilterGroups = getFilterGroups;
43
- function getFunctionId(fullName) {
44
- return fullName.split("/")[5];
45
- }
46
- exports.getFunctionId = getFunctionId;
47
- function getRegion(fullName) {
48
- return fullName.split("/")[3];
49
- }
50
- function getFunctionLabel(fnOrName) {
51
- if (typeof fnOrName === "string") {
52
- return getFunctionId(fnOrName) + "(" + getRegion(fnOrName) + ")";
53
- }
54
- else {
55
- return `${fnOrName.id}(${fnOrName.region})`;
56
- }
38
+ function getFunctionLabel(fn) {
39
+ return `${fn.id}(${fn.region})`;
57
40
  }
58
41
  exports.getFunctionLabel = getFunctionLabel;
59
- function logAndTrackDeployStats(queue, errorHandler) {
60
- const stats = queue.stats();
61
- logger_1.logger.debug(`Total Function Deployment time: ${stats.elapsed}`);
62
- logger_1.logger.debug(`${stats.total} Functions Deployed`);
63
- logger_1.logger.debug(`${errorHandler.errors.length} Functions Errored`);
64
- logger_1.logger.debug(`Average Function Deployment time: ${stats.avg}`);
65
- if (stats.total > 0) {
66
- if (errorHandler.errors.length === 0) {
67
- track("functions_deploy_result", "success", stats.total);
68
- }
69
- else if (errorHandler.errors.length < stats.total) {
70
- track("functions_deploy_result", "partial_success", stats.total - errorHandler.errors.length);
71
- track("functions_deploy_result", "partial_failure", errorHandler.errors.length);
72
- track("functions_deploy_result", "partial_error_ratio", errorHandler.errors.length / stats.total);
73
- }
74
- else {
75
- track("functions_deploy_result", "failure", stats.total);
76
- }
77
- }
78
- }
79
- exports.logAndTrackDeployStats = logAndTrackDeployStats;
80
- function printSuccess(func, type) {
81
- utils.logSuccess(clc.bold.green("functions[" + getFunctionLabel(func) + "]: ") +
82
- "Successful " +
83
- type +
84
- " operation. ");
85
- }
86
- exports.printSuccess = printSuccess;
87
- async function printTriggerUrls(context, want) {
88
- const have = await backend.existingBackend(context, true);
89
- const httpsFunctions = have.cloudFunctions.filter((fn) => {
90
- if (backend.isEventTrigger(fn.trigger)) {
91
- return false;
92
- }
93
- return want.cloudFunctions.some(backend.sameFunctionName(fn));
94
- });
95
- if (httpsFunctions.length === 0) {
96
- return;
97
- }
98
- for (const httpsFunc of httpsFunctions) {
99
- if (!httpsFunc.uri) {
100
- logger_1.logger.debug("Missing URI for HTTPS function in printTriggerUrls. This shouldn't happen");
101
- continue;
102
- }
103
- logger_1.logger.info(clc.bold("Function URL"), `(${getFunctionLabel(httpsFunc)}):`, httpsFunc.uri);
104
- }
105
- }
106
- exports.printTriggerUrls = printTriggerUrls;