Skip to content

Commit 2481ece

Browse files
authored
[BREAKING(functions)] Update post-deploy function logic to stop aggressively deleting conatiners (#8324)
1 parent 75cca8e commit 2481ece

File tree

5 files changed

+547
-24
lines changed

5 files changed

+547
-24
lines changed

src/deploy/functions/prompts.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { logger } from "../../logger";
77
import * as backend from "./backend";
88
import * as pricing from "./pricing";
99
import * as utils from "../../utils";
10+
import * as artifacts from "../../functions/artifacts";
1011
import { Options } from "../../options";
1112
import { EndpointUpdate } from "./release/planner";
1213

@@ -270,3 +271,45 @@ export async function promptForMinInstances(
270271
throw new FirebaseError("Deployment canceled.", { exit: 1 });
271272
}
272273
}
274+
275+
/**
276+
* Prompt users for days before containers are cleanuped up by the cleanup policy.
277+
*/
278+
export async function promptForCleanupPolicyDays(
279+
options: Options,
280+
locations: string[],
281+
): Promise<number> {
282+
utils.logLabeledWarning(
283+
"functions",
284+
`No cleanup policy detected for repositories in ${locations.join(", ")}. ` +
285+
"This may result in a small monthly bill as container images accumulate over time.",
286+
);
287+
288+
if (options.force) {
289+
return artifacts.DEFAULT_CLEANUP_DAYS;
290+
}
291+
292+
if (options.nonInteractive) {
293+
throw new FirebaseError(
294+
`Functions successfully deployed but could not set up cleanup policy in ` +
295+
`${locations.length > 1 ? "locations" : "location"} ${locations.join(", ")}. ` +
296+
`Pass the --force option to automatically set up a cleanup policy or ` +
297+
"run 'firebase functions:artifacts:setpolicy' to manually set up a cleanup policy.",
298+
);
299+
}
300+
301+
const result = await promptOnce({
302+
type: "input",
303+
name: "days",
304+
default: artifacts.DEFAULT_CLEANUP_DAYS.toString(),
305+
message: "How many days do you want to keep container images before they're deleted?",
306+
validate: (input) => {
307+
const days = parseInt(input);
308+
if (isNaN(days) || days < 0) {
309+
return "Please enter a non-negative number";
310+
}
311+
return true;
312+
},
313+
});
314+
return parseInt(result);
315+
}

src/deploy/functions/release/index.ts

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ import * as clc from "colorette";
33
import { Options } from "../../../options";
44
import { logger } from "../../../logger";
55
import { reduceFlat } from "../../../functional";
6+
import * as utils from "../../../utils";
67
import * as args from "../args";
78
import * as backend from "../backend";
8-
import * as containerCleaner from "../containerCleaner";
99
import * as planner from "./planner";
1010
import * as fabricator from "./fabricator";
1111
import * as reporter from "./reporter";
1212
import * as executor from "./executor";
1313
import * as prompts from "../prompts";
14-
import * as experiments from "../../../experiments";
1514
import { getAppEngineLocation } from "../../../functionsConfig";
1615
import { getFunctionLabel } from "../functionsDeployHelper";
1716
import { FirebaseError } from "../../../error";
1817
import { getProjectNumber } from "../../../getProjectNumber";
1918
import { release as extRelease } from "../../extensions";
19+
import * as artifacts from "../../../functions/artifacts";
2020

2121
/** Releases new versions of functions and extensions to prod. */
2222
export async function release(
@@ -104,13 +104,11 @@ export async function release(
104104
const wantBackend = backend.merge(...Object.values(payload.functions).map((p) => p.wantBackend));
105105
printTriggerUrls(wantBackend);
106106

107-
const haveEndpoints = backend.allEndpoints(wantBackend);
108-
const deletedEndpoints = Object.values(plan)
109-
.map((r) => r.endpointsToDelete)
110-
.reduce(reduceFlat, []);
111-
if (experiments.isEnabled("automaticallydeletegcfartifacts")) {
112-
await containerCleaner.cleanupBuildImages(haveEndpoints, deletedEndpoints);
113-
}
107+
await setupArtifactCleanupPolicies(
108+
options,
109+
options.projectId!,
110+
Object.keys(wantBackend.endpoints),
111+
);
114112

115113
const allErrors = summary.results.filter((r) => r.error).map((r) => r.error) as Error[];
116114
if (allErrors.length) {
@@ -144,3 +142,65 @@ export function printTriggerUrls(results: backend.Backend): void {
144142
logger.info(clc.bold("Function URL"), `(${getFunctionLabel(httpsFunc)}):`, httpsFunc.uri);
145143
}
146144
}
145+
146+
/**
147+
* Sets up artifact cleanup policies for the regions where functions are deployed
148+
* and automatically sets up policies where needed.
149+
*
150+
* The policy is only set up when:
151+
* 1. No cleanup policy exists yet
152+
* 2. No other cleanup policies exist (beyond our own if we previously set one)
153+
* 3. User has not explicitly opted out
154+
*
155+
* In non-interactive mode:
156+
* - With force flag: applies the default cleanup policy
157+
* - Without force flag: warns and aborts deployment
158+
*/
159+
async function setupArtifactCleanupPolicies(
160+
options: Options,
161+
projectId: string,
162+
locations: string[],
163+
): Promise<void> {
164+
if (locations.length === 0) {
165+
return;
166+
}
167+
168+
const { locationsToSetup, locationsWithErrors: locationsWithCheckErrors } =
169+
await artifacts.checkCleanupPolicy(projectId, locations);
170+
171+
if (locationsToSetup.length === 0) {
172+
return;
173+
}
174+
175+
const daysToKeep = await prompts.promptForCleanupPolicyDays(options, locationsToSetup);
176+
177+
utils.logLabeledBullet(
178+
"functions",
179+
`Configuring cleanup policy for ${locationsToSetup.length > 1 ? "repositories" : "repository"} in ${locationsToSetup.join(", ")}. ` +
180+
`Images older than ${daysToKeep} days will be automatically deleted.`,
181+
);
182+
183+
const { locationsWithPolicy, locationsWithErrors: locationsWithSetupErrors } =
184+
await artifacts.setCleanupPolicies(projectId, locationsToSetup, daysToKeep);
185+
186+
utils.logLabeledBullet(
187+
"functions",
188+
`Configured cleanup policy for ${locationsWithPolicy.length > 1 ? "repositories" : "repository"} in ${locationsToSetup.join(", ")}.`,
189+
);
190+
191+
const locationsWithErrors = [...locationsWithCheckErrors, ...locationsWithSetupErrors];
192+
if (locationsWithErrors.length > 0) {
193+
utils.logLabeledWarning(
194+
"functions",
195+
`Failed to set up cleanup policy for repositories in ${locationsWithErrors.length > 1 ? "regions" : "region"} ` +
196+
`${locationsWithErrors.join(", ")}.` +
197+
"This could result in a small monthly bill as container images accumulate over time.",
198+
);
199+
throw new FirebaseError(
200+
`Functions successfully deployed but could not set up cleanup policy in ` +
201+
`${locationsWithErrors.length > 1 ? "regions" : "region"} ${locationsWithErrors.join(", ")}.` +
202+
`Pass the --force option to automatically set up a cleanup policy or` +
203+
"run 'firebase functions:artifacts:setpolicy' to set up a cleanup policy to automatically delete old images.",
204+
);
205+
}
206+
}

src/experiments.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,6 @@ export const ALL_EXPERIMENTS = experiments({
5858
public: true,
5959
},
6060

61-
// permanent experiment
62-
automaticallydeletegcfartifacts: {
63-
shortDescription: "Control whether functions cleans up images after deploys",
64-
fullDescription:
65-
"To control costs, Firebase defaults to automatically deleting containers " +
66-
"created during the build process. This has the side-effect of preventing " +
67-
"users from rolling back to previous revisions using the Run API. To change " +
68-
`this behavior, call ${bold("experiments:disable deletegcfartifactsondeploy")} ` +
69-
`consider also calling ${bold("experiments:enable deletegcfartifacts")} ` +
70-
`to enable the new command ${bold("functions:deletegcfartifacts")} which` +
71-
"lets you clean up images manually",
72-
public: true,
73-
default: true,
74-
},
75-
7661
// Emulator experiments
7762
emulatoruisnapshot: {
7863
shortDescription: "Load pre-release versions of the emulator UI",

0 commit comments

Comments
 (0)