No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

containerCleaner.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.DockerHelper = exports.deleteGcfArtifacts = exports.listGcfPaths = exports.ContainerRegistryCleaner = exports.NoopArtifactRegistryCleaner = exports.ArtifactRegistryCleaner = exports.cleanupBuildImages = void 0;
  4. const clc = require("colorette");
  5. const error_1 = require("../../error");
  6. const api_1 = require("../../api");
  7. const logger_1 = require("../../logger");
  8. const artifactregistry = require("../../gcp/artifactregistry");
  9. const backend = require("./backend");
  10. const docker = require("../../gcp/docker");
  11. const utils = require("../../utils");
  12. const poller = require("../../operation-poller");
  13. async function retry(func) {
  14. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  15. const MAX_RETRIES = 3;
  16. const INITIAL_BACKOFF = 100;
  17. const TIMEOUT_MS = 10000;
  18. let retry = 0;
  19. while (true) {
  20. try {
  21. const timeout = new Promise((resolve, reject) => {
  22. setTimeout(() => reject(new Error("Timeout")), TIMEOUT_MS);
  23. });
  24. return await Promise.race([func(), timeout]);
  25. }
  26. catch (err) {
  27. logger_1.logger.debug("Failed docker command with error ", err);
  28. retry += 1;
  29. if (retry >= MAX_RETRIES) {
  30. throw new error_1.FirebaseError("Failed to clean up artifacts", { original: err });
  31. }
  32. await sleep(Math.pow(INITIAL_BACKOFF, retry - 1));
  33. }
  34. }
  35. }
  36. async function cleanupBuildImages(haveFunctions, deletedFunctions, cleaners = {}) {
  37. utils.logBullet(clc.bold(clc.cyan("functions: ")) + "cleaning up build files...");
  38. const failedDomains = new Set();
  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) => {
  61. try {
  62. await gcrCleaner.cleanupFunction(func);
  63. }
  64. catch (err) {
  65. const path = `${func.project}/${docker.GCR_SUBDOMAIN_MAPPING[func.region]}/gcf`;
  66. failedDomains.add(`https://console.cloud.google.com/gcr/images/${path}`);
  67. }
  68. }));
  69. await Promise.all(cleanup);
  70. if (failedDomains.size) {
  71. let message = "Unhandled error cleaning up build images. This could result in a small monthly bill if not corrected. ";
  72. message +=
  73. "You can attempt to delete these images by redeploying or you can delete them manually at";
  74. if (failedDomains.size === 1) {
  75. message += " " + failedDomains.values().next().value;
  76. }
  77. else {
  78. message += [...failedDomains].map((domain) => "\n\t" + domain).join("");
  79. }
  80. utils.logLabeledWarning("functions", message);
  81. }
  82. }
  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;
  132. class ContainerRegistryCleaner {
  133. constructor() {
  134. this.helpers = {};
  135. }
  136. helper(location) {
  137. const subdomain = docker.GCR_SUBDOMAIN_MAPPING[location] || "us";
  138. if (!this.helpers[subdomain]) {
  139. const origin = `https://${subdomain}.${api_1.containerRegistryDomain}`;
  140. this.helpers[subdomain] = new DockerHelper(origin);
  141. }
  142. return this.helpers[subdomain];
  143. }
  144. async cleanupFunction(func) {
  145. const helper = this.helper(func.region);
  146. const uuids = (await helper.ls(`${func.project}/gcf/${func.region}`)).children;
  147. const uuidTags = {};
  148. const loadUuidTags = [];
  149. for (const uuid of uuids) {
  150. loadUuidTags.push((async () => {
  151. const path = `${func.project}/gcf/${func.region}/${uuid}`;
  152. const tags = (await helper.ls(path)).tags;
  153. uuidTags[path] = tags;
  154. })());
  155. }
  156. await Promise.all(loadUuidTags);
  157. const extractFunction = /^(.*)_version-\d+$/;
  158. const entry = Object.entries(uuidTags).find(([, tags]) => {
  159. return tags.find((tag) => { var _a; return ((_a = extractFunction.exec(tag)) === null || _a === void 0 ? void 0 : _a[1]) === func.id; });
  160. });
  161. if (!entry) {
  162. logger_1.logger.debug("Could not find image for function", backend.functionName(func));
  163. return;
  164. }
  165. await helper.rm(entry[0]);
  166. }
  167. }
  168. exports.ContainerRegistryCleaner = ContainerRegistryCleaner;
  169. function getHelper(cache, subdomain) {
  170. if (!cache[subdomain]) {
  171. cache[subdomain] = new DockerHelper(`https://${subdomain}.${api_1.containerRegistryDomain}`);
  172. }
  173. return cache[subdomain];
  174. }
  175. async function listGcfPaths(projectId, locations, dockerHelpers = {}) {
  176. if (!locations) {
  177. locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
  178. }
  179. const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
  180. if (invalidRegion) {
  181. throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
  182. }
  183. const locationsSet = new Set(locations);
  184. const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
  185. const failedSubdomains = [];
  186. const listAll = [];
  187. for (const subdomain of subdomains) {
  188. listAll.push((async () => {
  189. try {
  190. return getHelper(dockerHelpers, subdomain).ls(`${projectId}/gcf`);
  191. }
  192. catch (err) {
  193. failedSubdomains.push(subdomain);
  194. logger_1.logger.debug(err);
  195. const stat = {
  196. children: [],
  197. digests: [],
  198. tags: [],
  199. };
  200. return Promise.resolve(stat);
  201. }
  202. })());
  203. }
  204. const gcfDirs = (await Promise.all(listAll))
  205. .map((results) => results.children)
  206. .reduce((acc, val) => [...acc, ...val], [])
  207. .filter((loc) => locationsSet.has(loc));
  208. if (failedSubdomains.length === subdomains.size) {
  209. throw new error_1.FirebaseError("Failed to search all subdomains.");
  210. }
  211. else if (failedSubdomains.length > 0) {
  212. throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`);
  213. }
  214. return gcfDirs.map((loc) => {
  215. return `${docker.GCR_SUBDOMAIN_MAPPING[loc]}.${api_1.containerRegistryDomain}/${projectId}/gcf/${loc}`;
  216. });
  217. }
  218. exports.listGcfPaths = listGcfPaths;
  219. async function deleteGcfArtifacts(projectId, locations, dockerHelpers = {}) {
  220. if (!locations) {
  221. locations = Object.keys(docker.GCR_SUBDOMAIN_MAPPING);
  222. }
  223. const invalidRegion = locations.find((loc) => !docker.GCR_SUBDOMAIN_MAPPING[loc]);
  224. if (invalidRegion) {
  225. throw new error_1.FirebaseError(`Invalid region ${invalidRegion} supplied`);
  226. }
  227. const subdomains = new Set(Object.values(docker.GCR_SUBDOMAIN_MAPPING));
  228. const failedSubdomains = [];
  229. const deleteLocations = locations.map((loc) => {
  230. const subdomain = docker.GCR_SUBDOMAIN_MAPPING[loc];
  231. try {
  232. return getHelper(dockerHelpers, subdomain).rm(`${projectId}/gcf/${loc}`);
  233. }
  234. catch (err) {
  235. failedSubdomains.push(subdomain);
  236. logger_1.logger.debug(err);
  237. }
  238. });
  239. await Promise.all(deleteLocations);
  240. if (failedSubdomains.length === subdomains.size) {
  241. throw new error_1.FirebaseError("Failed to search all subdomains.");
  242. }
  243. else if (failedSubdomains.length > 0) {
  244. throw new error_1.FirebaseError(`Failed to search the following subdomains: ${failedSubdomains.join(",")}`);
  245. }
  246. }
  247. exports.deleteGcfArtifacts = deleteGcfArtifacts;
  248. class DockerHelper {
  249. constructor(origin) {
  250. this.cache = {};
  251. this.client = new docker.Client(origin);
  252. }
  253. async ls(path) {
  254. if (!(path in this.cache)) {
  255. this.cache[path] = retry(() => this.client.listTags(path)).then((res) => {
  256. return {
  257. tags: res.tags,
  258. digests: Object.keys(res.manifest),
  259. children: res.child,
  260. };
  261. });
  262. }
  263. return this.cache[path];
  264. }
  265. async rm(path) {
  266. let toThrowLater = undefined;
  267. const stat = await this.ls(path);
  268. const recursive = stat.children.map(async (child) => {
  269. try {
  270. await this.rm(`${path}/${child}`);
  271. stat.children.splice(stat.children.indexOf(child), 1);
  272. }
  273. catch (err) {
  274. toThrowLater = err;
  275. }
  276. });
  277. const deleteTags = stat.tags.map(async (tag) => {
  278. try {
  279. await retry(() => this.client.deleteTag(path, tag));
  280. stat.tags.splice(stat.tags.indexOf(tag), 1);
  281. }
  282. catch (err) {
  283. logger_1.logger.debug("Got error trying to remove docker tag:", err);
  284. toThrowLater = err;
  285. }
  286. });
  287. await Promise.all(deleteTags);
  288. const deleteImages = stat.digests.map(async (digest) => {
  289. try {
  290. await retry(() => this.client.deleteImage(path, digest));
  291. stat.digests.splice(stat.digests.indexOf(digest), 1);
  292. }
  293. catch (err) {
  294. logger_1.logger.debug("Got error trying to remove docker image:", err);
  295. toThrowLater = err;
  296. }
  297. });
  298. await Promise.all(deleteImages);
  299. await Promise.all(recursive);
  300. if (toThrowLater) {
  301. throw toThrowLater;
  302. }
  303. }
  304. }
  305. exports.DockerHelper = DockerHelper;