Skip to content

Commit 9a5d584

Browse files
committed
Replace container cleanup logic with cleanup policy check after function deploys.
1 parent 602d90f commit 9a5d584

File tree

4 files changed

+129
-26
lines changed

4 files changed

+129
-26
lines changed

Diff for: src/commands/functions-artifacts-setpolicy.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export const command = new Command("functions:artifacts:setpolicy")
2626
)
2727
.option(
2828
"--days <days>",
29-
"Number of days to keep container images before deletion. Default is 3 days.",
30-
"3",
29+
`Number of days to keep container images before deletion. Default is ${artifacts.DEFAULT_CLEANUP_DAYS} day.`,
30+
`${artifacts.DEFAULT_CLEANUP_DAYS}`,
3131
)
3232
.option(
3333
"--none",
@@ -51,7 +51,7 @@ export const command = new Command("functions:artifacts:setpolicy")
5151
.action(async (options: any) => {
5252
const projectId = needProjectId(options);
5353
const location = options.location || "us-central1";
54-
let daysToKeep = parseInt(options.days || "3", 10);
54+
let daysToKeep = parseInt(options.days, 10);
5555

5656
const repoPath = artifacts.makeRepoPath(projectId, location);
5757
let repository: artifactregistry.Repository;

Diff for: src/deploy/functions/release/index.ts

+121-8
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ 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 artifactregistry from "../../../gcp/artifactregistry";
20+
import * as artifacts from "../../../functions/artifacts";
2021

2122
/** Releases new versions of functions and extensions to prod. */
2223
export async function release(
@@ -105,12 +106,8 @@ export async function release(
105106
printTriggerUrls(wantBackend);
106107

107108
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-
}
109+
110+
await checkArtifactCleanupPolicies(options.projectId!, haveEndpoints);
114111

115112
const allErrors = summary.results.filter((r) => r.error).map((r) => r.error) as Error[];
116113
if (allErrors.length) {
@@ -144,3 +141,119 @@ export function printTriggerUrls(results: backend.Backend): void {
144141
logger.info(clc.bold("Function URL"), `(${getFunctionLabel(httpsFunc)}):`, httpsFunc.uri);
145142
}
146143
}
144+
145+
/**
146+
* Checks if artifact cleanup policies are set for the regions where functions are deployed
147+
* and automatically sets up policies where needed.
148+
*
149+
* The policy is only set up when:
150+
* 1. No cleanup policy exists yet
151+
* 2. No other cleanup policies exist (beyond our own if we previously set one)
152+
* 3. User has not explicitly opted out
153+
*/
154+
async function checkArtifactCleanupPolicies(
155+
projectId: string,
156+
endpoints: backend.Endpoint[],
157+
): Promise<void> {
158+
if (endpoints.length === 0) {
159+
return;
160+
}
161+
162+
const uniqueRegions = new Set<string>();
163+
for (const endpoint of endpoints) {
164+
uniqueRegions.add(endpoint.region);
165+
}
166+
167+
const regionResults = await Promise.all(
168+
Array.from(uniqueRegions).map(async (region) => {
169+
try {
170+
const repoPath = artifacts.makeRepoPath(projectId, region);
171+
const repository = await artifactregistry.getRepository(repoPath);
172+
const existingPolicy = artifacts.findExistingPolicy(repository);
173+
const hasPolicy = !!existingPolicy;
174+
const hasOptOut = artifacts.hasCleanupOptOut(repository);
175+
176+
// Check if there are any other cleanup policies beyond our own
177+
const hasOtherPolicies =
178+
repository.cleanupPolicies &&
179+
Object.keys(repository.cleanupPolicies).some(
180+
(key) => key !== artifacts.CLEANUP_POLICY_ID,
181+
);
182+
183+
return {
184+
region,
185+
repository,
186+
hasPolicy,
187+
hasOptOut,
188+
hasOtherPolicies,
189+
};
190+
} catch (err) {
191+
logger.debug(`Failed to check artifact cleanup policy for region ${region}:`, err);
192+
return {
193+
region,
194+
hasPolicy: false,
195+
hasOptOut: false,
196+
hasOtherPolicies: false,
197+
error: err,
198+
};
199+
}
200+
}),
201+
);
202+
203+
const regionsWithErrors = regionResults
204+
.filter((result) => result.error)
205+
.map((result) => result.region);
206+
207+
const regionsToSetup = regionResults.filter(
208+
(result) => !result.hasPolicy && !result.hasOptOut && !result.hasOtherPolicies && !result.error,
209+
);
210+
211+
const regionsNeedingWarning: string[] = [];
212+
213+
if (regionsToSetup.length > 0) {
214+
utils.logLabeledSuccess(
215+
"functions",
216+
`Configuring a cleanup policy for repositories in ${regionsToSetup.join(", ")}. ` +
217+
`Images older than ${artifacts.DEFAULT_CLEANUP_DAYS} days will be automatically deleted.`,
218+
);
219+
const setupResults = await Promise.all(
220+
regionsToSetup.map(async (result) => {
221+
try {
222+
logger.debug(`Setting up artifact cleanup policy for region ${result.region}`);
223+
await artifacts.setCleanupPolicy(result.repository, artifacts.DEFAULT_CLEANUP_DAYS);
224+
return { region: result.region, success: true };
225+
} catch (err) {
226+
logger.debug(
227+
`Failed to set up artifact cleanup policy for region ${result.region}:`,
228+
err,
229+
);
230+
regionsNeedingWarning.push(result.region);
231+
return { region: result.region, success: false, error: err };
232+
}
233+
}),
234+
);
235+
236+
const failedSetups = setupResults.filter((r) => !r.success);
237+
if (failedSetups.length > 0) {
238+
logger.debug(
239+
`Failed to set up artifact cleanup policies for ${failedSetups.length} regions:`,
240+
failedSetups.map((f) => f.region).join(", "),
241+
);
242+
}
243+
}
244+
245+
const regionsToWarn = [...regionsNeedingWarning, ...regionsWithErrors];
246+
247+
if (regionsToWarn.length > 0) {
248+
utils.logLabeledWarning(
249+
"functions",
250+
`No cleanup policy detected for repositories in ${regionsToWarn.length > 1 ? "regions" : "region"} ` +
251+
`${regionsToWarn.join(", ")}. ` +
252+
"This could result in a small monthly bill as container images accumulate over time.",
253+
);
254+
utils.logLabeledBullet(
255+
"functions",
256+
"Run 'firebase functions:artifacts:setpolicy' to set up a cleanup policy to automatically delete old images.",
257+
);
258+
}
259+
}

Diff for: src/experiments.ts

-15
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",

Diff for: src/functions/artifacts.ts

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export const CLEANUP_POLICY_ID = "firebase-functions-cleanup";
1717
*/
1818
export const OPT_OUT_LABEL_KEY = "firebase-functions-cleanup-opted-out";
1919

20+
/**
21+
* Default number of days to keep container images for cleanup policies
22+
*/
23+
export const DEFAULT_CLEANUP_DAYS = 1;
24+
2025
const SECONDS_IN_DAY = 24 * 60 * 60;
2126

2227
/**

0 commit comments

Comments
 (0)