"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DockerHelper = exports.deleteGcfArtifacts = exports.listGcfPaths = exports.ContainerRegistryCleaner = exports.NoopArtifactRegistryCleaner = exports.ArtifactRegistryCleaner = exports.cleanupBuildImages = void 0; const clc = require("colorette"); const error_1 = require("../../error"); const api_1 = require("../../api"); const logger_1 = require("../../logger"); const artifactregistry = require("../../gcp/artifactregistry"); const backend = require("./backend"); const docker = require("../../gcp/docker"); const utils = require("../../utils"); const poller = require("../../operation-poller"); async function retry(func) { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const MAX_RETRIES = 3; const INITIAL_BACKOFF = 100; const TIMEOUT_MS = 10000; let retry = 0; while (true) { try { const timeout = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Timeout")), TIMEOUT_MS); }); return await Promise.race([func(), timeout]); } catch (err) { logger_1.logger.debug("Failed docker command with error ", err); retry += 1; if (retry >= MAX_RETRIES) { throw new error_1.FirebaseError("Failed to clean up artifacts", { original: err }); } await sleep(Math.pow(INITIAL_BACKOFF, retry - 1)); } } } async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}) { utils.logBullet(clc.bold(clc.cyan("functions: ")) + "cleaning up build files..."); const failedDomains = new Set(); const cleanup = []; const arCleaner = cleaners.ar || new ArtifactRegistryCleaner(); cleanup.push(...haveFunctions.map(async (func) => { try { await arCleaner.cleanupFunction(func); } catch (err) { const path = `${func.project}/${func.region}/gcf-artifacts`; failedDomains.add(`https://console.cloud.google.com/artifacts/docker/${path}`); } })); cleanup.push(...deletedFunctions.map(async (func) => { try { await Promise.all([arCleaner.cleanupFunction(func), arCleaner.cleanupFunctionCache(func)]); } catch (err) { const path = `${func.project}/${func.region}/gcf-artifacts`; failedDomains.add(`https://console.cloud.google.com/artifacts/docker/${path}`); } })); const gcrCleaner = cleaners.gcr || new ContainerRegistryCleaner(); cleanup.push(...[...haveFunctions, ...deletedFunctions].map(async (func) => { try { await gcrCleaner.cleanupFunction(func); } catch (err) { const path = `${func.project}/${docker.GCR_SUBDOMAIN_MAPPING[func.region]}/gcf`; failedDomains.add(`https://console.cloud.google.com/gcr/images/${path}`); } })); await Promise.all(cleanup); if (failedDomains.size) { let message = "Unhandled error cleaning up build images. This could result in a small monthly bill if not corrected. "; message += "You can attempt to delete these images by redeploying or you can delete them manually at"; if (failedDomains.size === 1) { message += " " + failedDomains.values().next().value; } else { message += [...failedDomains].map((domain) => "\n\t" + domain).join(""); } utils.logLabeledWarning("functions", message); } } exports.cleanupBuildImages = cleanupBuildImages; class ArtifactRegistryCleaner { static packagePath(func) { const encodedId = func.id .replace(/_/g, "__") .replace(/-/g, "--") .replace(/^[A-Z]/, (first) => `${first.toLowerCase()}-${first.toLowerCase()}`) .replace(/[A-Z]/g, (upper) => `_${upper.toLowerCase()}`); return `projects/${func.project}/locations/${func.region}/repositories/gcf-artifacts/packages/${encodedId}`; } async cleanupFunction(func) { let op; try { op = await artifactregistry.deletePackage(ArtifactRegistryCleaner.packagePath(func)); } catch (err) { if (err.status === 404) { return; } throw err; } if (op.done) { return; } await poller.pollOperation(Object.assign(Object.assign({}, ArtifactRegistryCleaner.POLLER_OPTIONS), { pollerName: `cleanup-${func.region}-${func.id}`, operationResourceName: op.name })); } async cleanupFunctionCache(func) { const op = await artifactregistry.deletePackage(`${ArtifactRegistryCleaner.packagePath(func)}%2Fcache`); if (op.done) { return; } await poller.pollOperation(Object.assign(Object.assign({}, ArtifactRegistryCleaner.POLLER_OPTIONS), { pollerName: `cleanup-cache-${func.region}-${func.id}`, operationResourceName: op.name })); } } exports.ArtifactRegistryCleaner = ArtifactRegistryCleaner; ArtifactRegistryCleaner.POLLER_OPTIONS = { apiOrigin: api_1.artifactRegistryDomain, apiVersion: artifactregistry.API_VERSION, masterTimeout: 5 * 60 * 1000, }; class NoopArtifactRegistryCleaner extends ArtifactRegistryCleaner { cleanupFunction() { return Promise.resolve(); } cleanupFunctionCache() { return Promise.resolve(); } } exports.NoopArtifactRegistryCleaner = NoopArtifactRegistryCleaner; class ContainerRegistryCleaner { constructor() { this.helpers = {}; } helper(location) { const subdomain = docker.GCR_SUBDOMAIN_MAPPING[location] || "us"; if (!this.helpers[subdomain]) { const origin = `https://${subdomain}.${api_1.containerRegistryDomain}`; this.helpers[subdomain] = new DockerHelper(origin); } return this.helpers[subdomain]; } async cleanupFunction(func) { const helper = this.helper(func.region); const uuids = (await helper.ls(`${func.project}/gcf/${func.region}`)).children; const uuidTags = {}; const loadUuidTags = []; for (const uuid of uuids) { loadUuidTags.push((async () => { const path = `${func.project}/gcf/${func.region}/${uuid}`; const tags = (await helper.ls(path)).tags; uuidTags[path] = tags; })()); } await Promise.all(loadUuidTags); const extractFunction = /^(.*)_version-\d+$/; const entry = Object.entries(uuidTags).find(([, tags]) => { return tags.find((tag) => { var _a; return ((_a = extractFunction.exec(tag)) === null || _a === void 0 ? void 0 : _a[1]) === func.id; }); }); if (!entry) { logger_1.logger.debug("Could not find image for function", backend.functionName(func)); return; } await helper.rm(entry[0]); } } exports.ContainerRegistryCleaner = ContainerRegistryCleaner; function getHelper(cache, subdomain) { if (!cache[subdomain]) { cache[subdomain] = new DockerHelper(`https://${subdomain}.${api_1.containerRegistryDomain}`); } return cache[subdomain]; } async function listGcfPaths(projectId, locations, dockerHelpers = {}) { if (!locations) { locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING); } const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]); if (invalidRegion) { throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`); } const locationsSet = new Set(locations); const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING)); const failedSubdomains = []; const listAll = []; for (const subdomain of subdomains) { listAll.push((async () => { try { return getHelper(dockerHelpers, subdomain).ls(`${projectId}/gcf`); } catch (err) { failedSubdomains.push(subdomain); logger_1.logger.debug(err); const stat = { children: [], digests: [], tags: [], }; return Promise.resolve(stat); } })()); } const gcfDirs = (await Promise.all(listAll)) .map((results) => results.children) .reduce((acc, val) => [...acc, ...val], []) .filter((loc) => locationsSet.has(loc)); if (failedSubdomains.length === subdomains.size) { throw new error_1.FirebaseError("Failed to search all subdomains."); } else if (failedSubdomains.length > 0) { throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`); } return gcfDirs.map((loc) => { return `${docker.GCR_SUBDOMAIN_MAPPING[loc]}.${api_1.containerRegistryDomain}/${projectId}/gcf/${loc}`; }); } exports.listGcfPaths = listGcfPaths; async function deleteGcfArtifacts(projectId, locations, dockerHelpers = {}) { if (!locations) { locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING); } const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]); if (invalidRegion) { throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`); } const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING)); const failedSubdomains = []; const deleteLocations = locations.map((loc) => { const subdomain = docker.GCR_SUBDOMAIN_MAPPING[loc]; try { return getHelper(dockerHelpers, subdomain).rm(`${projectId}/gcf/${loc}`); } catch (err) { failedSubdomains.push(subdomain); logger_1.logger.debug(err); } }); await Promise.all(deleteLocations); if (failedSubdomains.length === subdomains.size) { throw new error_1.FirebaseError("Failed to search all subdomains."); } else if (failedSubdomains.length > 0) { throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`); } } exports.deleteGcfArtifacts = deleteGcfArtifacts; class DockerHelper { constructor(origin) { this.cache = {}; this.client = new docker.Client(origin); } async ls(path) { if (!(path in this.cache)) { this.cache[path] = retry(() => this.client.listTags(path)).then((res) => { return { tags: res.tags, digests: Object.keys(res.manifest), children: res.child, }; }); } return this.cache[path]; } async rm(path) { let toThrowLater = undefined; const stat = await this.ls(path); const recursive = stat.children.map(async (child) => { try { await this.rm(`${path}/${child}`); stat.children.splice(stat.children.indexOf(child), 1); } catch (err) { toThrowLater = err; } }); const deleteTags = stat.tags.map(async (tag) => { try { await retry(() => this.client.deleteTag(path, tag)); stat.tags.splice(stat.tags.indexOf(tag), 1); } catch (err) { logger_1.logger.debug("Got error trying to remove docker tag:", err); toThrowLater = err; } }); await Promise.all(deleteTags); const deleteImages = stat.digests.map(async (digest) => { try { await retry(() => this.client.deleteImage(path, digest)); stat.digests.splice(stat.digests.indexOf(digest), 1); } catch (err) { logger_1.logger.debug("Got error trying to remove docker image:", err); toThrowLater = err; } }); await Promise.all(deleteImages); await Promise.all(recursive); if (toThrowLater) { throw toThrowLater; } } } exports.DockerHelper = DockerHelper;