UNPKG

@topgroup/diginext

Version:

A BUILD SERVER & CLI to deploy apps to any Kubernetes clusters.

703 lines (702 loc) 34.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.rollout = exports.previewPrerelease = exports.cleanUp = void 0; const chalk_1 = __importDefault(require("chalk")); const log_1 = require("diginext-utils/dist/xconsole/log"); const fs_1 = require("fs"); const js_yaml_1 = __importDefault(require("js-yaml")); const lodash_1 = require("lodash"); const path_1 = __importDefault(require("path")); const app_config_1 = require("../../app.config"); const config_1 = require("../../config/config"); const const_1 = require("../../config/const"); const plugins_1 = require("../../plugins"); const mongodb_1 = require("../../plugins/mongodb"); const slug_1 = require("../../plugins/slug"); const services_1 = require("../../services"); const generate_deployment_name_1 = __importDefault(require("../deploy/generate-deployment-name")); const index_1 = __importDefault(require("./index")); const kubectl_1 = require("./kubectl"); /** * Clean up PRERELEASE resources by ID or release data * @param idOrRelease - Release ID or {Release} data */ async function cleanUp(idOrRelease) { const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); let releaseData; // validation releaseData = await DB.findOne("release", { id: (0, mongodb_1.isValidObjectId)(idOrRelease) ? idOrRelease : idOrRelease._id }, { select: ["_id", "id", "slug", "workspace", "owner", "cluster", "appSlug", "projectSlug", "namespace"], populate: ["workspace", "owner"], }); if (!releaseData) throw new Error(`Release "${idOrRelease}" not found.`); const { cluster: clusterSlug, appSlug, namespace, owner, workspace } = releaseData; let cluster; // authenticate cluster's provider & switch kubectl to that cluster: try { cluster = await index_1.default.authClusterBySlug(clusterSlug, { ownership: { owner: owner, workspace: workspace } }); } catch (e) { (0, log_1.logError)(`[KUBE_DEPLOY] Clean up > `, e); return { error: e.message }; } const { contextName: context } = cluster; // Fallback support to the deprecated "main-app" name const app = await DB.findOne("app", { slug: appSlug }, { populate: ["project"] }); const deprecatedMainAppName = (0, slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); const mainAppName = await (0, generate_deployment_name_1.default)(app); // Clean up Prerelease YAML const cleanUpCommands = []; // Delete INGRESS to optimize cluster cleanUpCommands.push(index_1.default.deleteIngressByFilter(namespace, { context, skipOnError: true, filterLabel: `phase=prerelease,main-app=${mainAppName}`, })); // Delete Prerelease SERVICE to optimize cluster cleanUpCommands.push(index_1.default.deleteServiceByFilter(namespace, { context, filterLabel: `phase=prerelease,main-app=${mainAppName}` })); // Clean up Prerelease Deployments cleanUpCommands.push(index_1.default.deleteDeploymentsByFilter(namespace, { context, filterLabel: `phase=prerelease,main-app=${mainAppName}` })); // ! --- fallback support deprecated app name --- // Delete INGRESS (fallback support deprecated app name) if (deprecatedMainAppName) cleanUpCommands.push(index_1.default.deleteIngressByFilter(namespace, { context, skipOnError: true, filterLabel: `phase=prerelease,main-app=${deprecatedMainAppName}`, })); // ! --- fallback support deprecated app name --- // Delete Prerelease SERVICE to optimize cluster (fallback support deprecated app name) if (deprecatedMainAppName) cleanUpCommands.push(index_1.default.deleteServiceByFilter(namespace, { context, filterLabel: `phase=prerelease,main-app=${deprecatedMainAppName}` })); // ! --- fallback support deprecated app name --- // Clean up Prerelease Deployments if (deprecatedMainAppName) cleanUpCommands.push(index_1.default.deleteDeploymentsByFilter(namespace, { context, filterLabel: `phase=prerelease,main-app=${deprecatedMainAppName}` })); // Clean up immediately & just ignore if any errors for (const cmd of cleanUpCommands) { try { await cmd; } catch (e) { (0, log_1.logWarn)(`[CLEAN UP] Ignore command: ${e}`); } } // * Print success: let msg = `🎉 PRERELEASE DEPLOYMENT DELETED 🎉`; (0, log_1.logSuccess)(msg); return { error: null, data: releaseData }; } exports.cleanUp = cleanUp; /** * Roll out a prerelease environment * @param {String} id - Release ID */ async function previewPrerelease(id, options = {}) { const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); const { onUpdate } = options; let releaseData = await DB.findOne("release", { id }, { populate: ["owner", "workspace"], select: [ "_id", "id", "slug", "workspace", "owner", "cluster", "appSlug", "projectSlug", "namespace", "preYaml", "prereleaseUrl", "env", "build", ], }); const owner = releaseData.owner; const workspace = releaseData.workspace; // webhook const webhookSvc = new services_1.WebhookService(); webhookSvc.ownership = { owner, workspace }; const webhook = await DB.findOne("webhook", { release: id }); if ((0, lodash_1.isEmpty)(releaseData)) { const error = `Unable to roll out to PRE-RELEASE environment: Release not found.`; if (onUpdate) onUpdate(error); return { error }; } const { slug: releaseSlug, cluster: clusterSlug, appSlug, projectSlug, preYaml, prereleaseUrl, namespace, env } = releaseData; const app = await DB.findOne("app", { slug: appSlug }, { populate: ["project"] }); const mainAppName = await (0, generate_deployment_name_1.default)(app); (0, log_1.log)(`Preview the release: "${releaseSlug}" (${id})...`); if (onUpdate) onUpdate(`Rolling out to PRE-RELEASE environment: Release "${releaseSlug}" (${id})...`); let cluster; // authenticate cluster's provider & switch kubectl to that cluster: try { cluster = await index_1.default.authClusterBySlug(clusterSlug, { ownership: { owner, workspace } }); } catch (e) { const error = `Unable to roll out app to PRE-RELEASE environment: ${e}`; (0, log_1.logError)(error); if (onUpdate) onUpdate(error); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error }; } const { contextName: context } = cluster; if (!context) { const error = `Unable to roll out app to PRE-RELEASE environment: Cluster context not found.`; if (onUpdate) onUpdate(error); throw new Error(error); } /** * Check if there is any prod namespace, if not -> create one */ const isNsExisted = await index_1.default.isNamespaceExisted(namespace, { context }); if (!isNsExisted) { (0, log_1.log)(`[KUBE_DEPLOY] Namespace "${namespace}" not found, creating one...`); const createNsRes = await index_1.default.createNamespace(namespace, { context }); if (!createNsRes) { const errMsg = `Unable to create new namespace: ${namespace} (Cluster: ${clusterSlug} / Namespace: ${namespace} / App: ${appSlug} / Env: ${env})`; (0, log_1.logError)(errMsg); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error: errMsg }; } } /** * Create "imagePullSecrets" in a namespace */ try { const { name: imagePullSecretName } = await index_1.default.createImagePullSecretsInNamespace(appSlug, env, clusterSlug, namespace); if (onUpdate) onUpdate(`[PREVIEW] Created "${imagePullSecretName}" imagePullSecrets in the "${namespace}" namespace (cluster: "${clusterSlug}").`); } catch (e) { // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); const error = `[PREVIEW] Can't create "imagePullSecrets" in the "${namespace}" namespace (cluster: "${clusterSlug}").`; if (onUpdate) onUpdate(error); throw new Error(error); } /** * Delete current PRE-RELEASE deployments */ const curPrereleaseDeployments = await index_1.default.getDeploysByFilter(namespace, { context, filterLabel: `phase=prerelease,main-app=${mainAppName}`, }); if (!(0, lodash_1.isEmpty)(curPrereleaseDeployments)) { await index_1.default.deleteDeploymentsByFilter(namespace, { context, filterLabel: `phase=prerelease,main-app=${mainAppName}`, }); } /** * Apply PRE-RELEASE deployment YAML */ const prereleaseDeploymentRes = await index_1.default.kubectlApplyContent(preYaml, { context }); if (!prereleaseDeploymentRes) { // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); const error = `Can't preview the pre-release "${id}" (Cluster: ${clusterSlug} / Namespace: ${namespace} / App: ${appSlug} / Env: ${env}):\n${preYaml}`; if (onUpdate) onUpdate(error); throw new Error(error); } (0, log_1.logSuccess)(`The PRE-RELEASE environment is ready to preview: https://${prereleaseUrl}`); return { error: null, data: releaseData }; } exports.previewPrerelease = previewPrerelease; /** * Roll out a release * @param id - Release ID */ async function rollout(id, options = {}) { const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); const { onUpdate } = options; const { execa, execaCommand } = await Promise.resolve().then(() => __importStar(require("execa"))); let releaseData = await DB.findOne("release", { id }, { populate: ["owner", "workspace"] }); if ((0, lodash_1.isEmpty)(releaseData)) { const error = `Unable to roll out: Release "${id}" not found.`; if (onUpdate) onUpdate(error); return { error }; } const { slug: releaseSlug, projectSlug, // ! This is not PROJECT_ID of Google Cloud provider cluster: clusterSlug, appSlug, preYaml: prereleaseYaml, deploymentYaml, endpoint: endpointUrl, namespace, env, } = releaseData; // webhook const owner = releaseData.owner; const workspace = releaseData.workspace; const webhookSvc = new services_1.WebhookService(); webhookSvc.ownership = { owner, workspace }; const webhook = await DB.findOne("webhook", { release: id }); // log(`Rolling out the release: "${releaseSlug}" (ID: ${id})`); if (onUpdate) onUpdate(`Rolling out the release: "${releaseSlug}" (ID: ${id})`); // get the app const app = await DB.findOne("app", { slug: appSlug }, { populate: ["project"] }); if (!app && onUpdate) { const error = `Unable to roll out: app "${appSlug}" not found.`; onUpdate(error); return { error }; } // log(`Rolling out > app:`, app); const deprecatedMainAppName = (0, slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); const mainAppName = await (0, generate_deployment_name_1.default)(app); // log(`Rolling out > mainAppName:`, mainAppName); // authenticate cluster's provider & switch kubectl to that cluster: const cluster = await DB.findOne("cluster", { slug: clusterSlug }); if (!cluster) { (0, log_1.logError)(`Cluster "${clusterSlug}" not found.`); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error: `Cluster "${clusterSlug}" not found.` }; } try { await index_1.default.authCluster(cluster, { ownership: { owner, workspace } }); // log(`Rolling out > Checked connectivity of "${clusterSlug}" cluster.`); } catch (e) { const error = `Unable to authenticate the cluster: ${e.message}`; (0, log_1.logError)(`[ROLL_OUT] ${error}`); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error }; } const { contextName: context } = cluster; if (options === null || options === void 0 ? void 0 : options.isDebugging) (0, log_1.log)(`Rolling out > Connected to "${clusterSlug}" cluster.`); // create temporary directory to store release's yaml const tmpDir = path_1.default.resolve(const_1.CLI_DIR, `storage/releases/${releaseSlug}`); if (!(0, fs_1.existsSync)(tmpDir)) (0, fs_1.mkdirSync)(tmpDir, { recursive: true }); // ! NEW WAY -> LESS DOWNTIME WHEN ROLLING OUT NEW DEPLOYMENT ! /** * Check if there is any prod namespace, if not -> create one */ const isNsExisted = await index_1.default.isNamespaceExisted(namespace, { context }); if (!isNsExisted) { (0, log_1.log)(`Namespace "${namespace}" not found, creating one...`); if (onUpdate) onUpdate(`Namespace "${namespace}" not found, creating one...`); const createNsRes = await index_1.default.createNamespace(namespace, { context }); if (!createNsRes) { const err = `Unable to create new namespace: ${namespace} (Cluster: ${clusterSlug} / Namespace: ${namespace} / App: ${appSlug} / Env: ${env})`; (0, log_1.logError)(`[ROLL_OUT]`, err); if (onUpdate) onUpdate(err); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error: err }; } } // create "imagePullSecret" in namespace: try { const { name: imagePullSecretName } = await index_1.default.createImagePullSecretsInNamespace(appSlug, env, clusterSlug, namespace); if (onUpdate) onUpdate(`[ROLL OUT] Created "${imagePullSecretName}" imagePullSecrets in the "${namespace}" namespace (cluster: "${clusterSlug}").`); } catch (e) { const error = `[ROLL OUT] Can't create "imagePullSecrets" in the "${namespace}" namespace (cluster: "${clusterSlug}").`; // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error }; } /** * 1. Create SERVICE & INGRESS */ if (options === null || options === void 0 ? void 0 : options.isDebugging) console.log("[ROLL OUT] Deployment YAML :>> ", deploymentYaml); let replicas = 1, envVars = [], resourceQuota = {}, service, svcName, ingress, ingressName, deployment, deploymentName; js_yaml_1.default.loadAll(deploymentYaml, (doc) => { if (doc && doc.kind == "Ingress") { ingress = doc; ingressName = doc.metadata.name; } if (doc && doc.kind == "Service") { service = doc; svcName = doc.metadata.name; } if (doc && doc.kind == "Deployment") { replicas = doc.spec.replicas; envVars = doc.spec.template.spec.containers[0].env; resourceQuota = doc.spec.template.spec.containers[0].resources; deployment = doc; deploymentName = doc.metadata.name; } }); // log(`3`, { appSlug, service, svcName, ingress, ingressName, deploymentName }); // Always apply new service, since the PORT could be changed !!! const SVC_CONTENT = (0, plugins_1.objectToDeploymentYaml)(service); const applySvcRes = await index_1.default.kubectlApplyContent(SVC_CONTENT, { context }); if (!applySvcRes) { const error = `Unable to apply SERVICE "${service.metadata.name}" (Cluster: ${clusterSlug} / Namespace: ${namespace} / App: ${appSlug} / Env: ${env}):\n${SVC_CONTENT}`; if (onUpdate) onUpdate(error); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error }; } if (onUpdate) onUpdate(`Created new service named "${appSlug}".`); // check ingress domain has been used yet or not: let isDomainUsed = false, usedDomain, deleteIng; if (ingress) { const domains = ingress.spec.rules.map((rule) => rule.host) || []; // console.log("domains :>> ", domains); if (domains.length > 0) { const allIngresses = await index_1.default.getAllIngresses({ context }); allIngresses.filter((ing) => { domains.map((domain) => { if (ing.spec.rules.map((rule) => rule.host).includes(domain)) { isDomainUsed = true; usedDomain = domain; deleteIng = ing; } }); }); if (isDomainUsed) { await index_1.default.deleteIngress(deleteIng.metadata.name, deleteIng.metadata.namespace, { context }); if (onUpdate) onUpdate(`Domain "${usedDomain}" has been used before at "${deleteIng.metadata.namespace}" namespace -> Deleted "${deleteIng.metadata.name}" ingress to create a new one.`); } } } // log(`5`); let prereleaseApp, prereleaseAppName; if (env === "prod") { js_yaml_1.default.loadAll(prereleaseYaml, function (doc) { if (doc && doc.kind == "Service") prereleaseAppName = doc.spec.selector.app; if (doc && doc.kind == "Deployment") prereleaseApp = doc; }); if (!prereleaseAppName) { const error = `[ROLL OUT] PROD environment: "prereleaseAppName" is invalid.`; if (onUpdate) onUpdate(error); return { error }; } // if (onUpdate) onUpdate(`prereleaseAppName = ${prereleaseAppName}`); } /** * 2. Delete prerelease app if it contains "prerelease" (OLD WAY) * and apply new app for production */ // TODO: Check crashed / failed deployments -> delete them! let oldDeploys = await index_1.default.getDeploys(namespace, { context, filterLabel: `phase!=prerelease,main-app=${mainAppName}` }); const deprecatedMainAppDeploys = await index_1.default.getDeploys(namespace, { context, filterLabel: `phase!=prerelease,main-app=${deprecatedMainAppName}`, }); if (deprecatedMainAppDeploys && deprecatedMainAppDeploys.length > 0) oldDeploys.push(...deprecatedMainAppDeploys); if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`Current app deployments (to be deleted later on): ${oldDeploys.map((d) => d.metadata.name).join(",")}`); const createNewDeployment = async (appDoc) => { const newApp = appDoc; const newAppName = deploymentName; newApp.metadata.name = deploymentName; // labels newApp.metadata.labels.phase = "live"; // mark this app as "live" phase newApp.metadata.labels.project = projectSlug; newApp.metadata.labels.app = newAppName; newApp.metadata.labels["main-app"] = mainAppName; newApp.spec.template.metadata.labels.phase = "live"; newApp.spec.template.metadata.labels.app = newAppName; newApp.spec.template.metadata.labels["main-app"] = mainAppName; // envs & quotas newApp.spec.template.spec.containers[0].env = envVars; newApp.spec.template.spec.containers[0].resources = resourceQuota; // selector newApp.spec.selector.matchLabels.app = newAppName; let APP_CONTENT = (0, plugins_1.objectToDeploymentYaml)(newApp); const appCreateResult = await index_1.default.kubectlApplyContent(APP_CONTENT, { context }); if (!appCreateResult) { throw new Error(`[ROLL OUT] Failed to apply APP DEPLOYMENT config to "${newAppName}" in "${namespace}" namespace of "${context}" context:\n${APP_CONTENT}`); } if (onUpdate) onUpdate(`Created new deployment "${newAppName}" successfully.`); return newApp; }; if (deploymentName.indexOf("prerelease") > -1 || (0, lodash_1.isEmpty)(oldDeploys)) { // ! if "prerelease" was deployed in OLD WAY or there are no old deployments await createNewDeployment(deployment); } else { // ! if "prerelease" was deployed in NEW WAY -> add label "phase" = "live" try { const args = [ `--context=${context}`, "patch", "deploy", deploymentName, "-n", namespace, "--patch", `'{ "metadata": { "labels": { "phase": "live" } } }'`, ]; await execa(`kubectl`, args, config_1.cliOpts); if (onUpdate) onUpdate(`Updated "${deploymentName}" deployment successfully.`); } catch (e) { // if (onUpdate) onUpdate(`Patched "${deploymentName}" deployment failure: ${e.message}`); await createNewDeployment(deployment); } } /** * 3. [ONLY PROD DEPLOY] Update ENV variables to PRODUCTION values */ if (env === "prod" && !(0, lodash_1.isEmpty)(envVars)) { const setPreEnvVarRes = await index_1.default.setEnvVar(envVars, prereleaseAppName, namespace, { context }); if (setPreEnvVarRes) if (onUpdate) onUpdate(`Updated environment variables to "${prereleaseAppName}" deployment successfully.`); } // Wait until the deployment is ready! const isNewDeploymentReady = async () => { const newDeploys = await index_1.default.getDeploys(namespace, { context, filterLabel: `phase=live,app=${deploymentName}`, metrics: false }); // log(`${namespace} > ${deploymentName} > newDeploys :>>`, newDeploys); let isReady = false; newDeploys.forEach((deploy) => { var _a, _b; (0, log_1.log)(`[ROLL OUT] ${deploymentName} > deploy.status.conditions :>>`, deploy.status.conditions); // log(`[ROLL OUT] deploy.status.replicas :>>`, deploy.status.replicas); // log(`[ROLL OUT] deploy.status.unavailableReplicas :>>`, deploy.status.unavailableReplicas); // log(`[ROLL OUT] deploy.status.readyReplicas :>>`, deploy.status.readyReplicas); if (onUpdate) { (_b = (_a = deploy.status) === null || _a === void 0 ? void 0 : _a.conditions) === null || _b === void 0 ? void 0 : _b.map((condition) => { // if (condition.type === "False") isReady = true; // if (condition.type.toLowerCase() === "progressing") const msg = `[DEPLOY:${condition.type.toUpperCase()}] - ${condition.reason} - ${condition.message}`; onUpdate(msg); if (condition.type.toLowerCase() === "replicafailure") throw new Error(msg); }); } console.log(`[ROLL OUT] ${deploymentName} > deploy.status.readyReplicas :>> `, deploy.status.readyReplicas); console.log(`[ROLL OUT] ${deploymentName} > deploy.status.unavailableReplicas :>> `, deploy.status.unavailableReplicas); isReady = deploy.status.readyReplicas && deploy.status.readyReplicas >= 1; // if (deploy.status.unavailableReplicas && deploy.status.unavailableReplicas >= 1) { // isReady = false; // } else if (deploy.status.readyReplicas && deploy.status.readyReplicas >= 1) { // isReady = true; // } }); if (options === null || options === void 0 ? void 0 : options.isDebugging) (0, log_1.log)(`[ROLL OUT - INTERVAL] Checking new deployment's status -> Is Ready:`, isReady); return isReady; }; const isReallyReady = await (0, plugins_1.waitUntil)(isNewDeploymentReady, 10, 4 * 60); if (options === null || options === void 0 ? void 0 : options.isDebugging) (0, log_1.log)(`[ROLL OUT] Checking new deployment's status -> Is Fully Ready:`, isReallyReady); // TODO: check app's health instead of 15 seconds if (isReallyReady) { if (onUpdate) onUpdate(`App is being started up right now, please wait...`); // Wait another 15s to make sure app is not crashing... await (0, plugins_1.wait)(15 * 1000); } let isCrashed = false; const newDeploys = await index_1.default.getDeploys(namespace, { context, filterLabel: `phase=live,app=${deploymentName}`, metrics: false }); newDeploys.forEach((deploy) => { isCrashed = deploy.status.unavailableReplicas && deploy.status.unavailableReplicas >= 1; }); // Try to get the container logs and print to the web ui let containerLogs = await (0, kubectl_1.logPodByFilter)(namespace, { filterLabel: `app=${deploymentName}`, context }); if (!containerLogs) containerLogs += "\n\n-----\n\n" + (await (0, kubectl_1.logPodByFilter)(namespace, { filterLabel: `main-app=${mainAppName}`, context })); if (!containerLogs) containerLogs += "\n\n-----\n\n" + (await (0, kubectl_1.logPodByFilter)(namespace, { filterLabel: `app=${deploymentName}`, previous: true, context })); if (!containerLogs) containerLogs += "\n\n-----\n\n" + (await (0, kubectl_1.logPodByFilter)(namespace, { filterLabel: `main-app=${mainAppName}`, previous: true, context })); if (onUpdate && containerLogs) onUpdate(`--------------- APP'S LOGS ON STARTED UP --------------- \n${containerLogs}`); // throw the error if (!isReallyReady || isCrashed || containerLogs.indexOf("Error from server") > -1 || containerLogs.indexOf("An error occurred") > -1 || containerLogs.indexOf("Command failed") > -1) { const error = `[ERROR] The application failed to start up properly. To identify the issue, please review the application logs.`; if (onUpdate) onUpdate(error); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); return { error }; } /** * 4. Update "selector" of PRODUCTION SERVICE to select PRERELEASE APP NAME */ try { await execa(`kubectl`, [ `--context=${context}`, `patch`, "service", svcName, "-n", namespace, "--patch", `{ "spec": { "selector": { "app": "${deploymentName}" } } }`, ], config_1.cliOpts); if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`Patched "${svcName}" service successfully >> new deployment: ${deploymentName}`); } catch (e) { if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`[WARNING] Unable to patched "${svcName}" service: ${e.message}`); } /** * 5. Scale replicas to PRODUCTION config */ try { await execa("kubectl", [`--context=${context}`, "scale", `--replicas=${replicas}`, `deploy`, deploymentName, `-n`, namespace], config_1.cliOpts); if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`Scaled "${deploymentName}" replicas to ${replicas} successfully`); } catch (e) { if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`[WARNING] Unable to scale the replicas of "${deploymentName}" deployment to ${replicas}: ${e.message}`); } /** * 6. Apply resource quotas */ if (resourceQuota && resourceQuota.limits && resourceQuota.requests) { const resourcesStr = `--limits=cpu=${resourceQuota.limits.cpu},memory=${resourceQuota.limits.memory} --requests=cpu=${resourceQuota.requests.cpu},memory=${resourceQuota.requests.memory}`; const resouceCommand = `kubectl set resources deployment/${deploymentName} ${resourcesStr} -n ${namespace}`; try { await execaCommand(resouceCommand); if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`Applied resource quotas to ${deploymentName} successfully`); } catch (e) { if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`[WARNING] Command failed: ${resouceCommand}`); if (onUpdate && (options === null || options === void 0 ? void 0 : options.isDebugging)) onUpdate(`[WARNING] Applied "resources" quotas failure: ${e.message}`); } } // ! ALWAYS Create new ingress const ING_CONTENT = (0, plugins_1.objectToDeploymentYaml)(ingress); const ingCreateResult = await index_1.default.kubectlApplyContent(ING_CONTENT, { context }); if (!ingCreateResult) { const error = `[ERROR] Invalid INGRESS YAML (${env.toUpperCase()}) to "${ingressName}" in "${namespace}" namespace of "${context}" context:\n${ING_CONTENT}`; if (onUpdate) onUpdate(error); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(error); } // Print success: const prodUrlInCLI = chalk_1.default.bold(`https://${endpointUrl}`); const successMsg = `🎉 PUBLISHED AT: ${prodUrlInCLI} 🎉`; (0, log_1.logSuccess)(successMsg); if (onUpdate) onUpdate(successMsg); // Mark previous releases as "inactive": await DB.update("release", { appSlug, active: true }, { active: false }, { select: ["_id", "active", "appSlug"] }); // Mark this latest release as "active": const latestRelease = await DB.updateOne("release", { _id: id }, { active: true }, { select: ["_id", "active", "appSlug"] }); if (!latestRelease) { const error = `[ERROR] Unable to mark the latest release (${id}) status as "active".`; if (onUpdate) onUpdate(error); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(error); } // Assign this release as "latestRelease" of this app's deploy environment await DB.updateOne("app", { slug: appSlug }, { [`deployEnvironment.${env}.latestRelease`]: latestRelease._id }, { select: ["_id"] }); /** * 5. Clean up > Delete old deployments * - Skip CLEAN UP task on test environment */ if (!(0, app_config_1.IsTest)()) { if ((0, lodash_1.isArray)(oldDeploys) && oldDeploys.length > 0) { const waitTime = 2 * 60 * 1000; const oldDeploysCleanUpCommands = oldDeploys .filter((d) => d.metadata.name != deploymentName) .map((deploy) => { const deployName = deploy.metadata.name; return index_1.default.deleteDeploy(deployName, namespace, { context }); }); if (app_config_1.isServerMode) { setTimeout(async function (_commands) { try { await Promise.all(_commands); } catch (e) { (0, log_1.logWarn)(e.toString()); } }, waitTime, oldDeploysCleanUpCommands); } else { try { await Promise.all(oldDeploysCleanUpCommands); } catch (e) { (0, log_1.logWarn)(e.toString()); } } } /** * [ONLY WHEN DEPLOY TO PRODUCTION ENVIRONMENT] Clean up prerelease deployments (to optimize cluster resource quotas) */ if (app_config_1.isServerMode && env === "prod") { cleanUp(releaseData) .then(({ error }) => { if (error) throw new Error(`Unable to clean up PRERELEASE of release id [${id}]`); (0, log_1.logSuccess)(`✅ Clean up PRERELEASE of release id [${id}] SUCCESSFULLY.`); }) .catch((e) => (0, log_1.logError)(`Unable to clean up PRERELEASE of release id [${id}]:`, e)); } } return { error: null, data: releaseData }; } exports.rollout = rollout;