firebase-tools 9.20.0 → 9.23.1

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 (98) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/lib/api.js +2 -0
  3. package/lib/apiv2.js +7 -4
  4. package/lib/commands/crashlytics-symbols-upload.js +2 -2
  5. package/lib/commands/deploy.js +9 -1
  6. package/lib/commands/ext-configure.js +1 -1
  7. package/lib/commands/ext-dev-deprecate.js +63 -0
  8. package/lib/commands/ext-dev-undeprecate.js +56 -0
  9. package/lib/commands/ext-dev-unpublish.js +10 -3
  10. package/lib/commands/ext-export.js +44 -0
  11. package/lib/commands/ext-install.js +1 -1
  12. package/lib/commands/ext-update.js +1 -1
  13. package/lib/commands/functions-delete.js +55 -42
  14. package/lib/commands/functions-list.js +11 -11
  15. package/lib/commands/index.js +6 -5
  16. package/lib/commands/init.js +3 -0
  17. package/lib/config.js +3 -2
  18. package/lib/deploy/extensions/args.js +2 -0
  19. package/lib/deploy/extensions/deploy.js +49 -0
  20. package/lib/deploy/extensions/deploymentSummary.js +52 -0
  21. package/lib/deploy/extensions/errors.js +31 -0
  22. package/lib/deploy/extensions/index.js +8 -0
  23. package/lib/deploy/extensions/params.js +39 -0
  24. package/lib/deploy/extensions/planner.js +94 -0
  25. package/lib/deploy/extensions/prepare.js +111 -0
  26. package/lib/deploy/extensions/release.js +43 -0
  27. package/lib/deploy/extensions/secrets.js +150 -0
  28. package/lib/deploy/extensions/tasks.js +98 -0
  29. package/lib/deploy/extensions/validate.js +17 -0
  30. package/lib/deploy/functions/backend.js +84 -115
  31. package/lib/deploy/functions/checkIam.js +73 -12
  32. package/lib/deploy/functions/containerCleaner.js +97 -50
  33. package/lib/deploy/functions/deploy.js +4 -10
  34. package/lib/deploy/functions/eventTypes.js +10 -0
  35. package/lib/deploy/functions/functionsDeployHelper.js +3 -68
  36. package/lib/deploy/functions/prepare.js +72 -29
  37. package/lib/deploy/functions/pricing.js +17 -17
  38. package/lib/deploy/functions/prompts.js +22 -21
  39. package/lib/deploy/functions/release/executor.js +39 -0
  40. package/lib/deploy/functions/release/fabricator.js +425 -0
  41. package/lib/deploy/functions/release/index.js +73 -0
  42. package/lib/deploy/functions/release/planner.js +162 -0
  43. package/lib/deploy/functions/release/reporter.js +165 -0
  44. package/lib/deploy/functions/release/sourceTokenScraper.js +28 -0
  45. package/lib/deploy/functions/release/timer.js +14 -0
  46. package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +129 -126
  47. package/lib/deploy/functions/runtimes/node/parseTriggers.js +32 -54
  48. package/lib/deploy/functions/services/index.js +38 -0
  49. package/lib/deploy/functions/services/storage.js +43 -0
  50. package/lib/deploy/functions/triggerRegionHelper.js +9 -25
  51. package/lib/deploy/functions/validate.js +1 -24
  52. package/lib/deploy/index.js +10 -1
  53. package/lib/emulator/auth/apiSpec.js +37 -6
  54. package/lib/emulator/auth/operations.js +45 -17
  55. package/lib/emulator/auth/server.js +16 -2
  56. package/lib/emulator/auth/state.js +34 -15
  57. package/lib/emulator/auth/widget_ui.js +14 -0
  58. package/lib/emulator/downloadableEmulators.js +7 -7
  59. package/lib/emulator/functionsEmulator.js +18 -4
  60. package/lib/emulator/storage/cloudFunctions.js +37 -7
  61. package/lib/ensureApiEnabled.js +10 -12
  62. package/lib/extensions/askUserForParam.js +14 -6
  63. package/lib/extensions/checkProjectBilling.js +7 -7
  64. package/lib/extensions/export.js +107 -0
  65. package/lib/extensions/extensionsApi.js +103 -21
  66. package/lib/extensions/extensionsHelper.js +5 -2
  67. package/lib/extensions/listExtensions.js +16 -11
  68. package/lib/extensions/paramHelper.js +6 -4
  69. package/lib/extensions/provisioningHelper.js +16 -3
  70. package/lib/extensions/refs.js +9 -1
  71. package/lib/extensions/secretsUtils.js +10 -9
  72. package/lib/extensions/updateHelper.js +12 -2
  73. package/lib/extensions/versionHelper.js +14 -0
  74. package/lib/extensions/warnings.js +33 -1
  75. package/lib/gcp/artifactregistry.js +16 -0
  76. package/lib/gcp/cloudfunctions.js +25 -72
  77. package/lib/gcp/cloudfunctionsv2.js +46 -98
  78. package/lib/gcp/cloudscheduler.js +22 -16
  79. package/lib/gcp/cloudtasks.js +143 -0
  80. package/lib/gcp/docker.js +36 -2
  81. package/lib/gcp/location.js +44 -0
  82. package/lib/gcp/proto.js +2 -2
  83. package/lib/gcp/pubsub.js +1 -9
  84. package/lib/gcp/secretManager.js +27 -6
  85. package/lib/gcp/storage.js +48 -32
  86. package/lib/init/features/project.js +2 -1
  87. package/lib/previews.js +1 -1
  88. package/lib/projectUtils.js +10 -1
  89. package/lib/utils.js +30 -1
  90. package/package.json +5 -4
  91. package/schema/firebase-config.json +9 -0
  92. package/lib/deploy/functions/deploymentPlanner.js +0 -113
  93. package/lib/deploy/functions/deploymentTimer.js +0 -23
  94. package/lib/deploy/functions/errorHandler.js +0 -75
  95. package/lib/deploy/functions/release.js +0 -116
  96. package/lib/deploy/functions/tasks.js +0 -324
  97. package/lib/functions/listFunctions.js +0 -10
  98. package/lib/functionsDelete.js +0 -60
@@ -1,38 +1,29 @@
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.isTaskQueueTriggered = 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 if (isTaskQueueTriggered(endpoint)) {
20
+ return "taskQueue";
21
+ }
22
+ else {
23
+ throw new Error("Unexpected trigger type for endpoint " + JSON.stringify(endpoint));
32
24
  }
33
- return fn.trigger.eventType;
34
25
  }
35
- exports.triggerTag = triggerTag;
26
+ exports.endpointTriggerType = endpointTriggerType;
36
27
  function memoryOptionDisplayName(option) {
37
28
  return {
38
29
  128: "128MB",
@@ -58,39 +49,38 @@ function isScheduleTriggered(triggered) {
58
49
  return {}.hasOwnProperty.call(triggered, "scheduleTrigger");
59
50
  }
60
51
  exports.isScheduleTriggered = isScheduleTriggered;
52
+ function isTaskQueueTriggered(triggered) {
53
+ return {}.hasOwnProperty.call(triggered, "taskQueueTrigger");
54
+ }
55
+ exports.isTaskQueueTriggered = isTaskQueueTriggered;
61
56
  function empty() {
62
57
  return {
63
58
  requiredAPIs: {},
64
- endpoints: [],
65
- cloudFunctions: [],
66
- schedules: [],
67
- topics: [],
59
+ endpoints: {},
68
60
  environmentVariables: {},
69
61
  };
70
62
  }
71
63
  exports.empty = empty;
64
+ function of(...endpoints) {
65
+ const bkend = Object.assign({}, empty());
66
+ for (const endpoint of endpoints) {
67
+ bkend.endpoints[endpoint.region] = bkend.endpoints[endpoint.region] || {};
68
+ if (bkend.endpoints[endpoint.region][endpoint.id]) {
69
+ throw new Error("Trying to create a backend with the same endpiont twice");
70
+ }
71
+ bkend.endpoints[endpoint.region][endpoint.id] = endpoint;
72
+ }
73
+ return bkend;
74
+ }
75
+ exports.of = of;
72
76
  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);
77
+ return (Object.keys(backend.requiredAPIs).length == 0 && Object.keys(backend.endpoints).length === 0);
77
78
  }
78
79
  exports.isEmptyBackend = isEmptyBackend;
79
80
  function functionName(cloudFunction) {
80
81
  return `projects/${cloudFunction.project}/locations/${cloudFunction.region}/functions/${cloudFunction.id}`;
81
82
  }
82
83
  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
84
  function scheduleIdForFunction(cloudFunction) {
95
85
  return `firebase-schedule-${cloudFunction.id}-${cloudFunction.region}`;
96
86
  }
@@ -104,7 +94,7 @@ async function existingBackend(context, forceRefresh) {
104
94
  }
105
95
  exports.existingBackend = existingBackend;
106
96
  async function loadExistingBackend(ctx) {
107
- var _a, _b, _c, _d;
97
+ var _a;
108
98
  ctx.loadedExistingBackend = true;
109
99
  ctx.existingBackend = Object.assign({}, empty());
110
100
  ctx.unreachableRegions = {
@@ -113,32 +103,10 @@ async function loadExistingBackend(ctx) {
113
103
  };
114
104
  const gcfV1Results = await gcf.listAllFunctions(ctx.projectId);
115
105
  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
- }
106
+ const endpoint = gcf.endpointFromFunction(apiFunction);
107
+ ctx.existingBackend.endpoints[endpoint.region] =
108
+ ctx.existingBackend.endpoints[endpoint.region] || {};
109
+ ctx.existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
142
110
  }
143
111
  ctx.unreachableRegions.gcfV1 = gcfV1Results.unreachable;
144
112
  if (!previews_1.previews.functionsv2) {
@@ -149,52 +117,16 @@ async function loadExistingBackend(ctx) {
149
117
  gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
150
118
  }
151
119
  catch (err) {
152
- if (err.status === 404 && ((_b = err.message) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes("method not found"))) {
120
+ if (err.status === 404 && ((_a = err.message) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes("method not found"))) {
153
121
  return;
154
122
  }
155
123
  throw err;
156
124
  }
157
125
  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
- }
126
+ const endpoint = gcfV2.endpointFromFunction(apiFunction);
127
+ ctx.existingBackend.endpoints[endpoint.region] =
128
+ ctx.existingBackend.endpoints[endpoint.region] || {};
129
+ ctx.existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
198
130
  }
199
131
  ctx.unreachableRegions.gcfV2 = gcfV2Results.unreachable;
200
132
  }
@@ -205,12 +137,12 @@ async function checkAvailability(context, want) {
205
137
  }
206
138
  const gcfV1Regions = new Set();
207
139
  const gcfV2Regions = new Set();
208
- for (const fn of want.cloudFunctions) {
209
- if (fn.platform == "gcfv1") {
210
- gcfV1Regions.add(fn.region);
140
+ for (const ep of allEndpoints(want)) {
141
+ if (ep.platform == "gcfv1") {
142
+ gcfV1Regions.add(ep.region);
211
143
  }
212
144
  else {
213
- gcfV2Regions.add(fn.region);
145
+ gcfV2Regions.add(ep.region);
214
146
  }
215
147
  }
216
148
  const neededUnreachableV1 = ctx.unreachableRegions.gcfV1.filter((region) => gcfV1Regions.has(region));
@@ -237,6 +169,43 @@ async function checkAvailability(context, want) {
237
169
  }
238
170
  }
239
171
  exports.checkAvailability = checkAvailability;
172
+ function allEndpoints(backend) {
173
+ return Object.values(backend.endpoints).reduce((accum, perRegion) => {
174
+ return [...accum, ...Object.values(perRegion)];
175
+ }, []);
176
+ }
177
+ exports.allEndpoints = allEndpoints;
178
+ function someEndpoint(backend, predicate) {
179
+ for (const endpoints of Object.values(backend.endpoints)) {
180
+ if (Object.values(endpoints).some(predicate)) {
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+ exports.someEndpoint = someEndpoint;
187
+ function matchingBackend(backend, predicate) {
188
+ const filtered = Object.assign({}, empty());
189
+ for (const endpoint of allEndpoints(backend)) {
190
+ if (!predicate(endpoint)) {
191
+ continue;
192
+ }
193
+ filtered.endpoints[endpoint.region] = filtered.endpoints[endpoint.region] || {};
194
+ filtered.endpoints[endpoint.region][endpoint.id] = endpoint;
195
+ }
196
+ return filtered;
197
+ }
198
+ exports.matchingBackend = matchingBackend;
199
+ function regionalEndpoints(backend, region) {
200
+ return backend.endpoints[region] ? Object.values(backend.endpoints[region]) : [];
201
+ }
202
+ exports.regionalEndpoints = regionalEndpoints;
203
+ exports.hasEndpoint = (backend) => (endpoint) => {
204
+ return !!backend.endpoints[endpoint.region] && !!backend.endpoints[endpoint.region][endpoint.id];
205
+ };
206
+ exports.missingEndpoint = (backend) => (endpoint) => {
207
+ return !exports.hasEndpoint(backend)(endpoint);
208
+ };
240
209
  function compareFunctions(left, right) {
241
210
  if (left.platform != right.platform) {
242
211
  return right.platform < left.platform ? -1 : 1;
@@ -1,19 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkHttpIam = exports.checkServiceAccountIam = void 0;
3
+ exports.ensureServiceAgentRoles = exports.mergeBindings = exports.checkHttpIam = exports.checkServiceAccountIam = void 0;
4
4
  const cli_color_1 = require("cli-color");
5
5
  const logger_1 = require("../../logger");
6
6
  const functionsDeployHelper_1 = require("./functionsDeployHelper");
7
7
  const error_1 = require("../../error");
8
- const iam_1 = require("../../gcp/iam");
8
+ const iam = require("../../gcp/iam");
9
9
  const backend = require("./backend");
10
10
  const track = require("../../track");
11
+ const utils = require("../../utils");
12
+ const resourceManager_1 = require("../../gcp/resourceManager");
13
+ const services_1 = require("./services");
11
14
  const PERMISSION = "cloudfunctions.functions.setIamPolicy";
12
15
  async function checkServiceAccountIam(projectId) {
13
16
  const saEmail = `${projectId}@appspot.gserviceaccount.com`;
14
17
  let passed = false;
15
18
  try {
16
- const iamResult = await iam_1.testResourceIamPermissions("https://iam.googleapis.com", "v1", `projects/${projectId}/serviceAccounts/${saEmail}`, ["iam.serviceAccounts.actAs"]);
19
+ const iamResult = await iam.testResourceIamPermissions("https://iam.googleapis.com", "v1", `projects/${projectId}/serviceAccounts/${saEmail}`, ["iam.serviceAccounts.actAs"]);
17
20
  passed = iamResult.passed;
18
21
  }
19
22
  catch (err) {
@@ -28,20 +31,20 @@ async function checkServiceAccountIam(projectId) {
28
31
  }
29
32
  exports.checkServiceAccountIam = checkServiceAccountIam;
30
33
  async function checkHttpIam(context, options, payload) {
31
- const functions = payload.functions.backend.cloudFunctions;
32
34
  const filterGroups = context.filters || functionsDeployHelper_1.getFilterGroups(options);
33
- const httpFunctions = functions
34
- .filter((f) => !backend.isEventTrigger(f.trigger))
35
+ const httpEndpoints = backend
36
+ .allEndpoints(payload.functions.backend)
37
+ .filter(backend.isHttpsTriggered)
35
38
  .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) {
39
+ const existing = await backend.existingBackend(context);
40
+ const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
41
+ if (newHttpsEndpoints.length === 0) {
39
42
  return;
40
43
  }
41
- logger_1.logger.debug("[functions] found", newHttpFunctions.length, "new HTTP functions, testing setIamPolicy permission...");
44
+ logger_1.logger.debug("[functions] found", newHttpsEndpoints.length, "new HTTP functions, testing setIamPolicy permission...");
42
45
  let passed = true;
43
46
  try {
44
- const iamResult = await iam_1.testIamPermissions(context.projectId, [PERMISSION]);
47
+ const iamResult = await iam.testIamPermissions(context.projectId, [PERMISSION]);
45
48
  passed = iamResult.passed;
46
49
  }
47
50
  catch (e) {
@@ -51,9 +54,67 @@ async function checkHttpIam(context, options, payload) {
51
54
  if (!passed) {
52
55
  track("Error (User)", "deploy:functions:http_create_missing_iam");
53
56
  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- ") +
57
+ newHttpsEndpoints.map((func) => func.id).join("\n- ") +
55
58
  `\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
59
  }
57
60
  logger_1.logger.debug("[functions] found setIamPolicy permission, proceeding with deploy");
58
61
  }
59
62
  exports.checkHttpIam = checkHttpIam;
63
+ function reduceEventsToServices(services, endpoint) {
64
+ const service = services_1.serviceForEndpoint(endpoint);
65
+ if (service.requiredProjectBindings && !services.find((s) => s.name === service.name)) {
66
+ services.push(service);
67
+ }
68
+ return services;
69
+ }
70
+ function mergeBindings(policy, allRequiredBindings) {
71
+ for (const requiredBindings of allRequiredBindings) {
72
+ if (requiredBindings.length === 0) {
73
+ continue;
74
+ }
75
+ for (const requiredBinding of requiredBindings) {
76
+ const ndx = policy.bindings.findIndex((policyBinding) => policyBinding.role === requiredBinding.role);
77
+ if (ndx === -1) {
78
+ policy.bindings.push(requiredBinding);
79
+ continue;
80
+ }
81
+ requiredBinding.members.forEach((updatedMember) => {
82
+ if (!policy.bindings[ndx].members.find((member) => member === updatedMember)) {
83
+ policy.bindings[ndx].members.push(updatedMember);
84
+ }
85
+ });
86
+ }
87
+ }
88
+ }
89
+ exports.mergeBindings = mergeBindings;
90
+ async function ensureServiceAgentRoles(projectId, want, have) {
91
+ const wantServices = backend.allEndpoints(want).reduce(reduceEventsToServices, []);
92
+ const haveServices = backend.allEndpoints(have).reduce(reduceEventsToServices, []);
93
+ const newServices = wantServices.filter((wantS) => !haveServices.find((haveS) => wantS.name === haveS.name));
94
+ if (newServices.length === 0) {
95
+ return;
96
+ }
97
+ let policy;
98
+ try {
99
+ policy = await resourceManager_1.getIamPolicy(projectId);
100
+ }
101
+ catch (err) {
102
+ utils.logLabeledBullet("functions", "Could not verify the necessary IAM configuration for the following newly-integrated services: " +
103
+ `${newServices.map((service) => service.api).join(", ")}` +
104
+ ". Deployment may fail.", "warn");
105
+ return;
106
+ }
107
+ const findRequiredBindings = [];
108
+ newServices.forEach((service) => findRequiredBindings.push(service.requiredProjectBindings(projectId, policy)));
109
+ const allRequiredBindings = await Promise.all(findRequiredBindings);
110
+ mergeBindings(policy, allRequiredBindings);
111
+ try {
112
+ await resourceManager_1.setIamPolicy(projectId, policy, "bindings");
113
+ }
114
+ catch (err) {
115
+ throw new error_1.FirebaseError("We failed to modify the IAM policy for the project. The functions " +
116
+ "deployment requires specific roles to be granted to service agents," +
117
+ " otherwise the deployment will fail.", { original: err });
118
+ }
119
+ }
120
+ exports.ensureServiceAgentRoles = ensureServiceAgentRoles;
@@ -1,38 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DockerHelper = exports.deleteGcfArtifacts = exports.listGcfPaths = exports.ContainerRegistryCleaner = exports.cleanupBuildImages = exports.SUBDOMAIN_MAPPING = void 0;
3
+ exports.DockerHelper = exports.deleteGcfArtifacts = exports.listGcfPaths = exports.ContainerRegistryCleaner = exports.NoopArtifactRegistryCleaner = exports.ArtifactRegistryCleaner = exports.cleanupBuildImages = void 0;
4
4
  const clc = require("cli-color");
5
+ const error_1 = require("../../error");
5
6
  const api_1 = require("../../api");
6
7
  const logger_1 = require("../../logger");
7
- const docker = require("../../gcp/docker");
8
+ const artifactregistry = require("../../gcp/artifactregistry");
8
9
  const backend = require("./backend");
10
+ const docker = require("../../gcp/docker");
9
11
  const utils = require("../../utils");
10
- const error_1 = require("../../error");
11
- exports.SUBDOMAIN_MAPPING = {
12
- "us-west2": "us",
13
- "us-west3": "us",
14
- "us-west4": "us",
15
- "us-central1": "us",
16
- "us-central2": "us",
17
- "us-east1": "us",
18
- "us-east4": "us",
19
- "northamerica-northeast1": "us",
20
- "southamerica-east1": "us",
21
- "europe-west1": "eu",
22
- "europe-west2": "eu",
23
- "europe-west3": "eu",
24
- "europe-west5": "eu",
25
- "europe-west6": "eu",
26
- "europe-central2": "eu",
27
- "asia-east1": "asia",
28
- "asia-east2": "asia",
29
- "asia-northeast1": "asia",
30
- "asia-northeast2": "asia",
31
- "asia-northeast3": "asia",
32
- "asia-south1": "asia",
33
- "asia-southeast2": "asia",
34
- "australia-southeast1": "asia",
35
- };
12
+ const poller = require("../../operation-poller");
36
13
  async function retry(func) {
37
14
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
38
15
  const MAX_RETRIES = 3;
@@ -47,7 +24,7 @@ async function retry(func) {
47
24
  return await Promise.race([func(), timeout]);
48
25
  }
49
26
  catch (error) {
50
- logger_1.logger.debug("Failed docker command with error", error);
27
+ logger_1.logger.debug("Failed docker command with error ", error);
51
28
  retry += 1;
52
29
  if (retry >= MAX_RETRIES) {
53
30
  throw new error_1.FirebaseError("Failed to clean up artifacts", { original: error });
@@ -56,19 +33,40 @@ async function retry(func) {
56
33
  }
57
34
  }
58
35
  }
59
- async function cleanupBuildImages(functions) {
36
+ async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}) {
60
37
  utils.logBullet(clc.bold.cyan("functions: ") + "cleaning up build files...");
61
- const gcrCleaner = new ContainerRegistryCleaner();
62
38
  const failedDomains = new Set();
63
- await Promise.all(functions.map((func) => (async () => {
39
+ const cleanup = [];
40
+ const arCleaner = cleaners.ar || new ArtifactRegistryCleaner();
41
+ cleanup.push(...haveFunctions.map(async (func) => {
42
+ try {
43
+ await arCleaner.cleanupFunction(func);
44
+ }
45
+ catch (err) {
46
+ const path = `${func.project}/${func.region}/gcf-artifacts`;
47
+ failedDomains.add(`https://console.cloud.google.com/artifacts/docker/${path}`);
48
+ }
49
+ }));
50
+ cleanup.push(...deletedFunctions.map(async (func) => {
51
+ try {
52
+ await Promise.all([arCleaner.cleanupFunction(func), arCleaner.cleanupFunctionCache(func)]);
53
+ }
54
+ catch (err) {
55
+ const path = `${func.project}/${func.region}/gcf-artifacts`;
56
+ failedDomains.add(`https://console.cloud.google.com/artifacts/docker/${path}`);
57
+ }
58
+ }));
59
+ const gcrCleaner = cleaners.gcr || new ContainerRegistryCleaner();
60
+ cleanup.push(...[...haveFunctions, ...deletedFunctions].map(async (func) => {
64
61
  try {
65
62
  await gcrCleaner.cleanupFunction(func);
66
63
  }
67
64
  catch (err) {
68
- const path = `${func.project}/${exports.SUBDOMAIN_MAPPING[func.region]}/gcf`;
65
+ const path = `${func.project}/${docker.GCR_SUBDOMAIN_MAPPING[func.region]}/gcf`;
69
66
  failedDomains.add(`https://console.cloud.google.com/gcr/images/${path}`);
70
67
  }
71
- })()));
68
+ }));
69
+ await Promise.all(cleanup);
72
70
  if (failedDomains.size) {
73
71
  let message = "Unhandled error cleaning up build images. This could result in a small monthly bill if not corrected. ";
74
72
  message +=
@@ -83,12 +81,60 @@ async function cleanupBuildImages(functions) {
83
81
  }
84
82
  }
85
83
  exports.cleanupBuildImages = cleanupBuildImages;
84
+ class ArtifactRegistryCleaner {
85
+ static packagePath(func) {
86
+ const encodedId = func.id
87
+ .replace(/_/g, "__")
88
+ .replace(/-/g, "--")
89
+ .replace(/^[A-Z]/, (first) => `${first.toLowerCase()}-${first.toLowerCase()}`)
90
+ .replace(/[A-Z]/g, (upper) => `_${upper.toLowerCase()}`);
91
+ return `projects/${func.project}/locations/${func.region}/repositories/gcf-artifacts/packages/${encodedId}`;
92
+ }
93
+ async cleanupFunction(func) {
94
+ let op;
95
+ try {
96
+ op = await artifactregistry.deletePackage(ArtifactRegistryCleaner.packagePath(func));
97
+ }
98
+ catch (err) {
99
+ if (err.status === 404) {
100
+ return;
101
+ }
102
+ throw err;
103
+ }
104
+ if (op.done) {
105
+ return;
106
+ }
107
+ await poller.pollOperation(Object.assign(Object.assign({}, ArtifactRegistryCleaner.POLLER_OPTIONS), { pollerName: `cleanup-${func.region}-${func.id}`, operationResourceName: op.name }));
108
+ }
109
+ async cleanupFunctionCache(func) {
110
+ const op = await artifactregistry.deletePackage(`${ArtifactRegistryCleaner.packagePath(func)}%2Fcache`);
111
+ if (op.done) {
112
+ return;
113
+ }
114
+ await poller.pollOperation(Object.assign(Object.assign({}, ArtifactRegistryCleaner.POLLER_OPTIONS), { pollerName: `cleanup-cache-${func.region}-${func.id}`, operationResourceName: op.name }));
115
+ }
116
+ }
117
+ exports.ArtifactRegistryCleaner = ArtifactRegistryCleaner;
118
+ ArtifactRegistryCleaner.POLLER_OPTIONS = {
119
+ apiOrigin: api_1.artifactRegistryDomain,
120
+ apiVersion: artifactregistry.API_VERSION,
121
+ masterTimeout: 5 * 60 * 1000,
122
+ };
123
+ class NoopArtifactRegistryCleaner extends ArtifactRegistryCleaner {
124
+ cleanupFunction() {
125
+ return Promise.resolve();
126
+ }
127
+ cleanupFunctionCache() {
128
+ return Promise.resolve();
129
+ }
130
+ }
131
+ exports.NoopArtifactRegistryCleaner = NoopArtifactRegistryCleaner;
86
132
  class ContainerRegistryCleaner {
87
133
  constructor() {
88
134
  this.helpers = {};
89
135
  }
90
136
  helper(location) {
91
- const subdomain = exports.SUBDOMAIN_MAPPING[location] || "us";
137
+ const subdomain = docker.GCR_SUBDOMAIN_MAPPING[location] || "us";
92
138
  if (!this.helpers[subdomain]) {
93
139
  const origin = `https://${subdomain}.${api_1.containerRegistryDomain}`;
94
140
  this.helpers[subdomain] = new DockerHelper(origin);
@@ -128,14 +174,14 @@ function getHelper(cache, subdomain) {
128
174
  }
129
175
  async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
130
176
  if (!locations) {
131
- locations = Object.keys(exports.SUBDOMAIN_MAPPING);
177
+ locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
132
178
  }
133
- const invalidRegion = locations.find((loc) => !exports.SUBDOMAIN_MAPPING[loc]);
179
+ const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
134
180
  if (invalidRegion) {
135
181
  throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
136
182
  }
137
183
  const locationsSet = new Set(locations);
138
- const subdomains = new Set(Object.values(exports.SUBDOMAIN_MAPPING));
184
+ const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
139
185
  const failedSubdomains = [];
140
186
  const listAll = [];
141
187
  for (const subdomain of subdomains) {
@@ -166,26 +212,27 @@ async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
166
212
  throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`);
167
213
  }
168
214
  return gcfDirs.map((loc) => {
169
- return `${exports.SUBDOMAIN_MAPPING[loc]}.${api_1.containerRegistryDomain}/${projectId}/gcf/${loc}`;
215
+ return `${docker.GCR_SUBDOMAIN_MAPPING[loc]}.${api_1.containerRegistryDomain}/${projectId}/gcf/${loc}`;
170
216
  });
171
217
  }
172
218
  exports.listGcfPaths = listGcfPaths;
173
219
  async function deleteGcfArtifacts(projectId, locations, dockerHelpers = {}) {
174
220
  if (!locations) {
175
- locations = Object.keys(exports.SUBDOMAIN_MAPPING);
221
+ locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
176
222
  }
177
- const invalidRegion = locations.find((loc) => !exports.SUBDOMAIN_MAPPING[loc]);
223
+ const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
178
224
  if (invalidRegion) {
179
225
  throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
180
226
  }
181
- const subdomains = new Set(Object.values(exports.SUBDOMAIN_MAPPING));
227
+ const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
182
228
  const failedSubdomains = [];
183
229
  const deleteLocations = locations.map((loc) => {
230
+ const subdomain = docker.GCR_SUBDOMAIN_MAPPING[loc];
184
231
  try {
185
- return getHelper(dockerHelpers, exports.SUBDOMAIN_MAPPING[loc]).rm(`${projectId}/gcf/${loc}`);
232
+ return getHelper(dockerHelpers, subdomain).rm(`${projectId}/gcf/${loc}`);
186
233
  }
187
234
  catch (err) {
188
- failedSubdomains.push(exports.SUBDOMAIN_MAPPING[loc]);
235
+ failedSubdomains.push(subdomain);
189
236
  logger_1.logger.debug(err);
190
237
  }
191
238
  });
@@ -217,7 +264,7 @@ class DockerHelper {
217
264
  async rm(path) {
218
265
  let toThrowLater = undefined;
219
266
  const stat = await this.ls(path);
220
- const recursive = stat.children.map((child) => (async () => {
267
+ const recursive = stat.children.map(async (child) => {
221
268
  try {
222
269
  await this.rm(`${path}/${child}`);
223
270
  stat.children.splice(stat.children.indexOf(child), 1);
@@ -225,8 +272,8 @@ class DockerHelper {
225
272
  catch (err) {
226
273
  toThrowLater = err;
227
274
  }
228
- })());
229
- const deleteTags = stat.tags.map((tag) => (async () => {
275
+ });
276
+ const deleteTags = stat.tags.map(async (tag) => {
230
277
  try {
231
278
  await retry(() => this.client.deleteTag(path, tag));
232
279
  stat.tags.splice(stat.tags.indexOf(tag), 1);
@@ -235,9 +282,9 @@ class DockerHelper {
235
282
  logger_1.logger.debug("Got error trying to remove docker tag:", err);
236
283
  toThrowLater = err;
237
284
  }
238
- })());
285
+ });
239
286
  await Promise.all(deleteTags);
240
- const deleteImages = stat.digests.map((digest) => (async () => {
287
+ const deleteImages = stat.digests.map(async (digest) => {
241
288
  try {
242
289
  await retry(() => this.client.deleteImage(path, digest));
243
290
  stat.digests.splice(stat.digests.indexOf(digest), 1);
@@ -246,7 +293,7 @@ class DockerHelper {
246
293
  logger_1.logger.debug("Got error trying to remove docker image:", err);
247
294
  toThrowLater = err;
248
295
  }
249
- })());
296
+ });
250
297
  await Promise.all(deleteImages);
251
298
  await Promise.all(recursive);
252
299
  if (toThrowLater) {