Skip to content

[BREAKING(functions)] Update post-deploy function logic to stop aggressively deleting conatiners #8324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/deploy/functions/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import * as backend from "./backend";
import * as pricing from "./pricing";
import * as utils from "../../utils";
import * as artifacts from "../../functions/artifacts";
import { Options } from "../../options";
import { EndpointUpdate } from "./release/planner";

Expand All @@ -14,8 +15,8 @@
* Checks if a deployment will create any functions with a failure policy
* or add a failure policy to an existing function.
* If there are any, prompts the user to acknowledge the retry behavior.
* @param options

Check warning on line 18 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc @param "options" description
* @param functions A list of all functions in the deployment

Check warning on line 19 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected @param names to be "options, want, have". Got "options, functions"
*/
export async function promptForFailurePolicies(
options: Options,
Expand Down Expand Up @@ -71,7 +72,7 @@
/**
* Checks if a deployment will delete any functions.
* If there are any, prompts the user if they should be deleted or not.
* @param options

Check warning on line 75 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Expected @param names to be "functionsToDelete, options". Got "options, functions"

Check warning on line 75 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc @param "options" description
* @param functions A list of functions to be deleted.
*/
export async function promptForFunctionDeletion(
Expand Down Expand Up @@ -122,7 +123,7 @@
* Cases include:
* Migrating from 2nd gen Firestore triggers to Firestore triggers with auth context
* @param fnsToUpdate An array of endpoint updates
* @param options

Check warning on line 126 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc @param "options" description
* @return An array of endpoints to proceed with updating
*/
export async function promptForUnsafeMigration(
Expand Down Expand Up @@ -233,7 +234,7 @@
.sort(backend.compareFunctions)
.map((fn) => {
return (
`\t${getFunctionLabel(fn)}: ${fn.minInstances} instances, ` +

Check warning on line 237 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "number | null | undefined" of template literal expression
backend.memoryOptionDisplayName(fn.availableMemoryMb || backend.DEFAULT_MEMORY) +
" of memory each"
);
Expand Down Expand Up @@ -270,3 +271,45 @@
throw new FirebaseError("Deployment canceled.", { exit: 1 });
}
}

/**
* Prompt users for days before containers are cleanuped up by the cleanup policy.
*/
export async function promptForCleanupPolicyDays(
options: Options,
locations: string[],
): Promise<number> {
utils.logLabeledWarning(
"functions",
`No cleanup policy detected for repositories in ${locations.join(", ")}. ` +
"This may result in a small monthly bill as container images accumulate over time.",
);

if (options.force) {
return artifacts.DEFAULT_CLEANUP_DAYS;
}

if (options.nonInteractive) {
throw new FirebaseError(
`Functions successfully deployed but could not set up cleanup policy in ` +
`${locations.length > 1 ? "locations" : "location"} ${locations.join(", ")}. ` +
`Pass the --force option to automatically set up a cleanup policy or ` +
"run 'firebase functions:artifacts:setpolicy' to manually set up a cleanup policy.",
);
}

const result = await promptOnce({
type: "input",
name: "days",
default: artifacts.DEFAULT_CLEANUP_DAYS.toString(),
message: "How many days do you want to keep container images before they're deleted?",
validate: (input) => {
const days = parseInt(input);

Check warning on line 307 in src/deploy/functions/prompts.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `string`
if (isNaN(days) || days < 0) {
return "Please enter a non-negative number";
}
return true;
},
});
return parseInt(result);
}
78 changes: 69 additions & 9 deletions src/deploy/functions/release/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
import { Options } from "../../../options";
import { logger } from "../../../logger";
import { reduceFlat } from "../../../functional";
import * as utils from "../../../utils";
import * as args from "../args";
import * as backend from "../backend";
import * as containerCleaner from "../containerCleaner";
import * as planner from "./planner";
import * as fabricator from "./fabricator";
import * as reporter from "./reporter";
import * as executor from "./executor";
import * as prompts from "../prompts";
import * as experiments from "../../../experiments";
import { getAppEngineLocation } from "../../../functionsConfig";
import { getFunctionLabel } from "../functionsDeployHelper";
import { FirebaseError } from "../../../error";
import { getProjectNumber } from "../../../getProjectNumber";
import { release as extRelease } from "../../extensions";
import * as artifacts from "../../../functions/artifacts";

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

const haveEndpoints = backend.allEndpoints(wantBackend);
const deletedEndpoints = Object.values(plan)
.map((r) => r.endpointsToDelete)
.reduce(reduceFlat, []);
if (experiments.isEnabled("automaticallydeletegcfartifacts")) {
await containerCleaner.cleanupBuildImages(haveEndpoints, deletedEndpoints);
}
await setupArtifactCleanupPolicies(
options,
options.projectId!,

Check warning on line 109 in src/deploy/functions/release/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
Object.keys(wantBackend.endpoints),
);

const allErrors = summary.results.filter((r) => r.error).map((r) => r.error) as Error[];
if (allErrors.length) {
Expand Down Expand Up @@ -144,3 +142,65 @@
logger.info(clc.bold("Function URL"), `(${getFunctionLabel(httpsFunc)}):`, httpsFunc.uri);
}
}

/**
* Sets up artifact cleanup policies for the regions where functions are deployed
* and automatically sets up policies where needed.
*
* The policy is only set up when:
* 1. No cleanup policy exists yet
* 2. No other cleanup policies exist (beyond our own if we previously set one)
* 3. User has not explicitly opted out
*
* In non-interactive mode:
* - With force flag: applies the default cleanup policy
* - Without force flag: warns and aborts deployment
*/
async function setupArtifactCleanupPolicies(
options: Options,
projectId: string,
locations: string[],
): Promise<void> {
if (locations.length === 0) {
return;
}

const { locationsToSetup, locationsWithErrors: locationsWithCheckErrors } =
await artifacts.checkCleanupPolicy(projectId, locations);

if (locationsToSetup.length === 0) {
return;
}

const daysToKeep = await prompts.promptForCleanupPolicyDays(options, locationsToSetup);

utils.logLabeledBullet(
"functions",
`Configuring cleanup policy for ${locationsToSetup.length > 1 ? "repositories" : "repository"} in ${locationsToSetup.join(", ")}. ` +
`Images older than ${daysToKeep} days will be automatically deleted.`,
);

const { locationsWithPolicy, locationsWithErrors: locationsWithSetupErrors } =
await artifacts.setCleanupPolicies(projectId, locationsToSetup, daysToKeep);

utils.logLabeledBullet(
"functions",
`Configured cleanup policy for ${locationsWithPolicy.length > 1 ? "repositories" : "repository"} in ${locationsToSetup.join(", ")}.`,
);

const locationsWithErrors = [...locationsWithCheckErrors, ...locationsWithSetupErrors];
if (locationsWithErrors.length > 0) {
utils.logLabeledWarning(
"functions",
`Failed to set up cleanup policy for repositories in ${locationsWithErrors.length > 1 ? "regions" : "region"} ` +
`${locationsWithErrors.join(", ")}.` +
"This could result in a small monthly bill as container images accumulate over time.",
);
throw new FirebaseError(
`Functions successfully deployed but could not set up cleanup policy in ` +
`${locationsWithErrors.length > 1 ? "regions" : "region"} ${locationsWithErrors.join(", ")}.` +
`Pass the --force option to automatically set up a cleanup policy or` +
"run 'firebase functions:artifacts:setpolicy' to set up a cleanup policy to automatically delete old images.",
);
}
}
15 changes: 0 additions & 15 deletions src/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,6 @@ export const ALL_EXPERIMENTS = experiments({
public: true,
},

// permanent experiment
automaticallydeletegcfartifacts: {
shortDescription: "Control whether functions cleans up images after deploys",
fullDescription:
"To control costs, Firebase defaults to automatically deleting containers " +
"created during the build process. This has the side-effect of preventing " +
"users from rolling back to previous revisions using the Run API. To change " +
`this behavior, call ${bold("experiments:disable deletegcfartifactsondeploy")} ` +
`consider also calling ${bold("experiments:enable deletegcfartifacts")} ` +
`to enable the new command ${bold("functions:deletegcfartifacts")} which` +
"lets you clean up images manually",
public: true,
default: true,
},

// Emulator experiments
emulatoruisnapshot: {
shortDescription: "Load pre-release versions of the emulator UI",
Expand Down
Loading
Loading