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.
- package/CHANGELOG.md +5 -1
- package/lib/api.js +2 -0
- package/lib/apiv2.js +7 -4
- package/lib/commands/crashlytics-symbols-upload.js +2 -2
- package/lib/commands/deploy.js +9 -1
- package/lib/commands/ext-configure.js +1 -1
- package/lib/commands/ext-dev-deprecate.js +63 -0
- package/lib/commands/ext-dev-undeprecate.js +56 -0
- package/lib/commands/ext-dev-unpublish.js +10 -3
- package/lib/commands/ext-export.js +44 -0
- package/lib/commands/ext-install.js +1 -1
- package/lib/commands/ext-update.js +1 -1
- package/lib/commands/functions-delete.js +55 -42
- package/lib/commands/functions-list.js +11 -11
- package/lib/commands/index.js +6 -5
- package/lib/commands/init.js +3 -0
- package/lib/config.js +3 -2
- package/lib/deploy/extensions/args.js +2 -0
- package/lib/deploy/extensions/deploy.js +49 -0
- package/lib/deploy/extensions/deploymentSummary.js +52 -0
- package/lib/deploy/extensions/errors.js +31 -0
- package/lib/deploy/extensions/index.js +8 -0
- package/lib/deploy/extensions/params.js +39 -0
- package/lib/deploy/extensions/planner.js +94 -0
- package/lib/deploy/extensions/prepare.js +111 -0
- package/lib/deploy/extensions/release.js +43 -0
- package/lib/deploy/extensions/secrets.js +150 -0
- package/lib/deploy/extensions/tasks.js +98 -0
- package/lib/deploy/extensions/validate.js +17 -0
- package/lib/deploy/functions/backend.js +84 -115
- package/lib/deploy/functions/checkIam.js +73 -12
- package/lib/deploy/functions/containerCleaner.js +97 -50
- package/lib/deploy/functions/deploy.js +4 -10
- package/lib/deploy/functions/eventTypes.js +10 -0
- package/lib/deploy/functions/functionsDeployHelper.js +3 -68
- package/lib/deploy/functions/prepare.js +72 -29
- package/lib/deploy/functions/pricing.js +17 -17
- package/lib/deploy/functions/prompts.js +22 -21
- package/lib/deploy/functions/release/executor.js +39 -0
- package/lib/deploy/functions/release/fabricator.js +425 -0
- package/lib/deploy/functions/release/index.js +73 -0
- package/lib/deploy/functions/release/planner.js +162 -0
- package/lib/deploy/functions/release/reporter.js +165 -0
- package/lib/deploy/functions/release/sourceTokenScraper.js +28 -0
- package/lib/deploy/functions/release/timer.js +14 -0
- package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +129 -126
- package/lib/deploy/functions/runtimes/node/parseTriggers.js +32 -54
- package/lib/deploy/functions/services/index.js +38 -0
- package/lib/deploy/functions/services/storage.js +43 -0
- package/lib/deploy/functions/triggerRegionHelper.js +9 -25
- package/lib/deploy/functions/validate.js +1 -24
- package/lib/deploy/index.js +10 -1
- package/lib/emulator/auth/apiSpec.js +37 -6
- package/lib/emulator/auth/operations.js +45 -17
- package/lib/emulator/auth/server.js +16 -2
- package/lib/emulator/auth/state.js +34 -15
- package/lib/emulator/auth/widget_ui.js +14 -0
- package/lib/emulator/downloadableEmulators.js +7 -7
- package/lib/emulator/functionsEmulator.js +18 -4
- package/lib/emulator/storage/cloudFunctions.js +37 -7
- package/lib/ensureApiEnabled.js +10 -12
- package/lib/extensions/askUserForParam.js +14 -6
- package/lib/extensions/checkProjectBilling.js +7 -7
- package/lib/extensions/export.js +107 -0
- package/lib/extensions/extensionsApi.js +103 -21
- package/lib/extensions/extensionsHelper.js +5 -2
- package/lib/extensions/listExtensions.js +16 -11
- package/lib/extensions/paramHelper.js +6 -4
- package/lib/extensions/provisioningHelper.js +16 -3
- package/lib/extensions/refs.js +9 -1
- package/lib/extensions/secretsUtils.js +10 -9
- package/lib/extensions/updateHelper.js +12 -2
- package/lib/extensions/versionHelper.js +14 -0
- package/lib/extensions/warnings.js +33 -1
- package/lib/gcp/artifactregistry.js +16 -0
- package/lib/gcp/cloudfunctions.js +25 -72
- package/lib/gcp/cloudfunctionsv2.js +46 -98
- package/lib/gcp/cloudscheduler.js +22 -16
- package/lib/gcp/cloudtasks.js +143 -0
- package/lib/gcp/docker.js +36 -2
- package/lib/gcp/location.js +44 -0
- package/lib/gcp/proto.js +2 -2
- package/lib/gcp/pubsub.js +1 -9
- package/lib/gcp/secretManager.js +27 -6
- package/lib/gcp/storage.js +48 -32
- package/lib/init/features/project.js +2 -1
- package/lib/previews.js +1 -1
- package/lib/projectUtils.js +10 -1
- package/lib/utils.js +30 -1
- package/package.json +5 -4
- package/schema/firebase-config.json +9 -0
- package/lib/deploy/functions/deploymentPlanner.js +0 -113
- package/lib/deploy/functions/deploymentTimer.js +0 -23
- package/lib/deploy/functions/errorHandler.js +0 -75
- package/lib/deploy/functions/release.js +0 -116
- package/lib/deploy/functions/tasks.js +0 -324
- package/lib/functions/listFunctions.js +0 -10
- 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.
|
|
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
|
|
10
|
-
|
|
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 ((
|
|
22
|
-
|
|
23
|
-
return "v1.callable";
|
|
24
|
-
}
|
|
25
|
-
return "v2.callable";
|
|
13
|
+
else if (isHttpsTriggered(endpoint)) {
|
|
14
|
+
return "https";
|
|
26
15
|
}
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return "
|
|
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.
|
|
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
|
|
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
|
|
117
|
-
ctx.existingBackend.
|
|
118
|
-
|
|
119
|
-
|
|
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 && ((
|
|
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
|
|
159
|
-
ctx.existingBackend.
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
209
|
-
if (
|
|
210
|
-
gcfV1Regions.add(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
34
|
-
.
|
|
35
|
+
const httpEndpoints = backend
|
|
36
|
+
.allEndpoints(payload.functions.backend)
|
|
37
|
+
.filter(backend.isHttpsTriggered)
|
|
35
38
|
.filter((f) => functionsDeployHelper_1.functionMatchesAnyGroup(f, filterGroups));
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
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",
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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}/${
|
|
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 =
|
|
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(
|
|
177
|
+
locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
|
|
132
178
|
}
|
|
133
|
-
const invalidRegion = locations.find((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(
|
|
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 `${
|
|
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(
|
|
221
|
+
locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
|
|
176
222
|
}
|
|
177
|
-
const invalidRegion = locations.find((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(
|
|
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,
|
|
232
|
+
return getHelper(dockerHelpers, subdomain).rm(`${projectId}/gcf/${loc}`);
|
|
186
233
|
}
|
|
187
234
|
catch (err) {
|
|
188
|
-
failedSubdomains.push(
|
|
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(
|
|
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(
|
|
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(
|
|
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) {
|