@topgroup/diginext
Version:
A BUILD SERVER & CLI to deploy apps to any Kubernetes clusters.
703 lines (702 loc) • 34.3 kB
JavaScript
;
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;