UNPKG

@topgroup/diginext

Version:

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

409 lines (408 loc) 20.7 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.deployWithBuildSlug = exports.deployBuild = exports.processDeployBuild = void 0; const lodash_1 = require("lodash"); const path_1 = __importDefault(require("path")); const app_config_1 = require("../../app.config"); const const_1 = require("../../config/const"); const array_1 = require("../../plugins/array"); const mongodb_1 = require("../../plugins/mongodb"); const services_1 = require("../../services"); const app_helper_1 = require("../apps/app-helper"); const get_app_environment_1 = require("../apps/get-app-environment"); const update_config_1 = require("../apps/update-config"); const build_1 = require("../build"); const update_release_status_1 = require("../build/update-release-status"); const k8s_1 = __importDefault(require("../k8s")); const create_build_slug_1 = require("./create-build-slug"); const generate_deployment_v2_1 = require("./generate-deployment-v2"); const processDeployBuild = async (build, release, cluster, options) => { const { env, owner, shouldUseFreshDeploy = false, skipReadyCheck = false, forceRollOut = false } = options; const { appSlug, projectSlug, tag: buildTag } = build; const { slug: username } = owner; const SOCKET_ROOM = (0, create_build_slug_1.createBuildSlug)({ projectSlug, appSlug, buildTag }); const releaseId = mongodb_1.MongoDB.toString(release._id); const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); const workspace = await DB.findOne("workspace", { _id: build.workspace }); // webhook const webhookSvc = new services_1.WebhookService(); webhookSvc.ownership = { owner, workspace }; const webhook = await DB.findOne("webhook", { release: releaseId }); // authenticate cluster & switch to that cluster's context try { await k8s_1.default.authCluster(cluster, { ownership: { owner, workspace } }); (0, build_1.sendLog)({ SOCKET_ROOM, message: `✓ Connected to "${cluster.name}" (context: ${cluster.contextName}).` }); } catch (e) { (0, build_1.sendLog)({ SOCKET_ROOM, message: `❌ Unable to connect the cluster: ${e.message}`, type: "error", action: "end" }); throw new Error(e.message); } // target environment info const { contextName: context } = cluster; const { namespace, endpoint } = release; /** * Create namespace & imagePullScrets here! * Because it will generate the name of secret to put into deployment yaml */ const isNsExisted = await k8s_1.default.isNamespaceExisted(namespace, { context }); if ((0, lodash_1.isUndefined)(isNsExisted)) { (0, build_1.sendLog)({ SOCKET_ROOM, message: `❌ Unable to connect cluster to get namespace list.`, type: "error", action: "end" }); throw new Error(`Unable to connect cluster to get namespace list.`); } if (!isNsExisted) { const createNsResult = await k8s_1.default.createNamespace(namespace, { context }); if (!createNsResult) throw new Error(`Unable to create new namespace: ${namespace}`); } /** * Checking "imagePullSecrets" in a namepsace */ try { const { name: imagePullSecretName } = await k8s_1.default.createImagePullSecretsInNamespace(appSlug, env, cluster.slug, namespace); (0, build_1.sendLog)({ SOCKET_ROOM, message: `Created "${imagePullSecretName}" imagePullSecrets in the "${namespace}" namespace.`, }); } catch (e) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", action: "end", message: `Can't create "imagePullSecrets" in the "${namespace}" namespace: ${e}`, }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(`Can't create "imagePullSecrets" in the "${namespace}" namespace.`); } /** * Checking NGINX Ingress: * - If there are a similar domain in different namespace -> throw error */ try { const allIngresses = await k8s_1.default.getAllIngresses({ context }); let namespaceOfExistingIngress; const ingInAnotherNamespace = allIngresses.find((ing) => { const findCondition = typeof ing.spec.rules.find((rule) => rule.host === endpoint) !== "undefined" && ing.metadata.namespace !== namespace; if (findCondition) namespaceOfExistingIngress = ing.metadata.namespace; return findCondition; }); if (ingInAnotherNamespace) { const message = `There is a similar domain (${endpoint}) in "${namespaceOfExistingIngress}" namespace of "${context}" cluster, unable to create new ingress with the same domain. Suggestions:\n- Delete the ingress of this domain "${endpoint}" in "${namespaceOfExistingIngress}" namepsace.\n- Use a different domain for this deploy environment.`; (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", action: "end", message }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(message); } } catch (e) { const message = `Unable to fetch ingresses of "${context}" cluster: ${e}`; (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", action: "end", message }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(message); } // Start rolling out new release /** * ! [WARNING] * ! If "--fresh" flag was specified, the deployment's namespace will be deleted & redeploy from scratch! */ // console.log("[DEPLOY BUILD] options.shouldUseFreshDeploy :>> ", options.shouldUseFreshDeploy); if (shouldUseFreshDeploy) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "warn", message: `[SYSTEM WARNING] Flag "--fresh" of CLI was specified by "${username}" while executed request deploy command, the build server's going to delete the "${namespace}" namespace (APP: ${appSlug} / PROJECT: ${projectSlug}) shortly...`, }); const wipedNamespaceRes = await k8s_1.default.deleteNamespaceByCluster(namespace, cluster.slug); if ((0, lodash_1.isEmpty)(wipedNamespaceRes)) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Unable to delete "${namespace}" namespace of "${cluster.slug}" cluster (APP: ${appSlug} / PROJECT: ${projectSlug}).`, }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(`Unable to delete "${namespace}" namespace of "${cluster.slug}" cluster (APP: ${appSlug} / PROJECT: ${projectSlug}).`); } (0, build_1.sendLog)({ SOCKET_ROOM, message: `Successfully deleted "${namespace}" namespace of "${cluster.slug}" cluster (APP: ${appSlug} / PROJECT: ${projectSlug}).`, }); } const onRolloutUpdate = (msg) => { // if any errors on rolling out -> stop processing deployment if (msg.indexOf("Error from server") > -1) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", action: "end", message: `[DEPLOY BUILD] Rollout > Error from server :>>\n${msg}` }); throw new Error(msg); } else { // if normal log message -> print out to the Web UI (0, build_1.sendLog)({ SOCKET_ROOM, message: msg }); } }; if (skipReadyCheck) { (0, build_1.sendLog)({ SOCKET_ROOM, message: env === "prod" ? `Rolling out the PRE-RELEASE deployment to "${env.toUpperCase()}" environment...` : `Rolling out the deployment to "${env.toUpperCase()}" environment...`, }); try { if (forceRollOut) { k8s_1.default.rollout(releaseId, { onUpdate: onRolloutUpdate }); } else { if (env === "prod") { k8s_1.default.previewPrerelease(releaseId, { onUpdate: onRolloutUpdate }); } else { k8s_1.default.rollout(releaseId, { onUpdate: onRolloutUpdate }); } } } catch (e) { const errMsg = `Failed to roll out the release :>> ${e.message}:`; (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", action: "end", message: errMsg }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(errMsg); } } else { if (release._id) { (0, build_1.sendLog)({ SOCKET_ROOM, message: env === "prod" ? `Rolling out the PRE-RELEASE deployment to "${env.toUpperCase()}" environment...` : `Rolling out the deployment to "${env.toUpperCase()}" environment...`, }); try { const result = env === "prod" ? forceRollOut ? await k8s_1.default.rollout(releaseId, { onUpdate: onRolloutUpdate }) : await k8s_1.default.previewPrerelease(releaseId, { onUpdate: onRolloutUpdate }) : await k8s_1.default.rollout(releaseId, { onUpdate: onRolloutUpdate }); if (result.error) { const errMsg = `Failed to roll out the release :>> ${result.error}.`; (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", message: errMsg, action: "end" }); throw new Error(errMsg); } release = result.data; (0, build_1.sendLog)({ SOCKET_ROOM, message: `✅ App has been deployed successfully!`, type: "success", action: "end" }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "success"); } catch (e) { const errMsg = `Failed to roll out the release :>> ${e.message}`; (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", action: "end", message: errMsg }); // dispatch/trigger webhook if (webhook) webhookSvc.trigger(mongodb_1.MongoDB.toString(webhook._id), "failed"); throw new Error(errMsg); } } } }; exports.processDeployBuild = processDeployBuild; const deployBuild = async (build, options) => { const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); // parse options const { env, owner, workspace, deployInBackground = true, cliVersion } = options; const { appSlug, projectSlug, tag: buildTag, num: buildNumber } = build; const { slug: username } = owner; const SOCKET_ROOM = (0, create_build_slug_1.createBuildSlug)({ projectSlug, appSlug, buildTag }); // build directory const SOURCE_CODE_DIR = `cache/${build.projectSlug}/${build.appSlug}/${build.branch}`; const buildDirectory = path_1.default.resolve(const_1.CLI_CONFIG_DIR, SOURCE_CODE_DIR); let app = await DB.updateOne("app", { slug: appSlug }, { updatedBy: owner._id }, { populate: ["project", "owner"] }); if (!app) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", message: `[DEPLOY BUILD] App "${appSlug}" not found.`, }); throw new Error(`[DEPLOY BUILD] App "${appSlug}" not found.`); } const project = app.project; const projectOwner = await DB.findOne("user", { _id: project.owner }); const appOwner = app.owner; // get deploy environment data let serverDeployEnvironment = await (0, get_app_environment_1.getDeployEvironmentByApp)(app, env); let isPassedDeployEnvironmentValidation = true; const errMsgs = []; // generate 'namespace' if it's not exists if (!serverDeployEnvironment.namespace) { const namespace = `${projectSlug}-${env || "dev"}`; await (0, update_config_1.updateAppConfig)(app, env, { namespace }); // reload app & deploy environment data... serverDeployEnvironment.namespace = namespace; app = await DB.findOne("app", { slug: appSlug }, { populate: ["project"] }); } // validate deploy environment data... if ((0, lodash_1.isEmpty)(serverDeployEnvironment)) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Deploy environment (${env.toUpperCase()}) of "${appSlug}" app is empty (probably deleted?).`, }); isPassedDeployEnvironmentValidation = false; errMsgs.push(`Deploy environment (${env.toUpperCase()}) of "${appSlug}" app is empty (probably deleted?).`); } if (!serverDeployEnvironment.cluster) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Deploy environment (${env.toUpperCase()}) of "${appSlug}" app doesn't contain "cluster" name (probably deleted?).`, }); isPassedDeployEnvironmentValidation = false; errMsgs.push(`Deploy environment (${env.toUpperCase()}) of "${appSlug}" app doesn't contain "cluster" name (probably deleted?).`); } if (!isPassedDeployEnvironmentValidation) throw new Error(errMsgs.join(",")); // find cluster const { cluster: clusterSlug } = serverDeployEnvironment; const cluster = await DB.findOne("cluster", { slug: clusterSlug }, { subpath: "/all" }); // get app config to generate deployment data const appConfig = (0, app_helper_1.getAppConfigFromApp)(app); /** * !!! IMPORTANT !!! * Generate deployment data (YAML) & save the YAML deployment to "app.environment[env]" * So it can be used to create release from build */ let deployment; (0, build_1.sendLog)({ SOCKET_ROOM, message: `[DEPLOY BUILD] Generating the deployment files on server...` }); try { deployment = await (0, generate_deployment_v2_1.generateDeploymentV2)({ appSlug, env, username, workspace, buildTag: buildTag, appConfig, targetDirectory: buildDirectory, }); } catch (e) { const errMsg = `[DEPLOY_BUILD] Generate YAML > error :>>\n${e.stack}`; // save log to database const { SystemLogService } = await Promise.resolve().then(() => __importStar(require("../../services"))); const logSvc = new SystemLogService({ owner, workspace }); logSvc.saveError(e, { name: "deploy-build" }); console.error(errMsg); (0, build_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Generate deployment YAML > error :>>\n${e.stack}`, action: "end" }); throw new Error(errMsg); } const { endpoint, deploymentContent } = deployment; // update data to deploy environment: serverDeployEnvironment.prereleaseUrl = null; serverDeployEnvironment.deploymentYaml = deploymentContent; serverDeployEnvironment.prereleaseDeploymentYaml = null; serverDeployEnvironment.updatedAt = new Date(); serverDeployEnvironment.lastUpdatedBy = username; // Update {user}, {project}, {environment} to database before rolling out const updatedAppData = { deployEnvironment: app.deployEnvironment || {} }; updatedAppData.lastUpdatedBy = username; updatedAppData.deployEnvironment[env] = serverDeployEnvironment; const updatedApp = await DB.updateOne("app", { slug: appSlug }, updatedAppData); // console.log("updatedApp.deployEnvironment[env].envVars :>> ", updatedApp.deployEnvironment[env].envVars); (0, build_1.sendLog)({ SOCKET_ROOM, message: `[DEPLOY BUILD] Generated the deployment files successfully!` }); // log(`[BUILD] App's last updated by "${updatedApp.lastUpdatedBy}".`); // update "deployStatus" of a build await DB.updateOne("build", { _id: build._id }, { deployStatus: "in_progress" }).catch(console.error); // Create new Release: // let prereleaseDeploymentData = fetchDeploymentFromContent(prereleaseDeploymentContent); let releaseId, newRelease; try { newRelease = await (0, build_1.createReleaseFromBuild)(build, env, { author: owner, workspace, cliVersion }); releaseId = mongodb_1.MongoDB.toString(newRelease._id); (0, build_1.sendLog)({ SOCKET_ROOM, message: `✓ Created new release "${SOCKET_ROOM}" (ID: ${releaseId}) on BUILD SERVER successfully.` }); } catch (e) { console.error("Deploy build > error :>> ", e); (0, build_1.sendLog)({ SOCKET_ROOM, message: `[DEPLOY BUILD] Create release from build failed: ${e.message}`, type: "error", action: "end" }); throw new Error(e.message); } // create webhook let webhook; const webhookSvc = new services_1.WebhookService(); webhookSvc.ownership = { owner, workspace }; if (app_config_1.isServerMode) { const consumers = (0, array_1.filterUniqueItems)([projectOwner === null || projectOwner === void 0 ? void 0 : projectOwner._id, appOwner === null || appOwner === void 0 ? void 0 : appOwner._id, owner === null || owner === void 0 ? void 0 : owner._id]) .filter((uid) => typeof uid !== "undefined") .map((uid) => mongodb_1.MongoDB.toString(uid)); // console.log("consumers :>> ", consumers); webhook = await webhookSvc.create({ events: ["deploy_status"], channels: ["email"], consumers, workspace: mongodb_1.MongoDB.toString(workspace._id), project: mongodb_1.MongoDB.toString(build.project), app: mongodb_1.MongoDB.toString(app._id), build: mongodb_1.MongoDB.toString(build._id), release: releaseId, }); } // process deploy build to cluster if (deployInBackground) { (0, exports.processDeployBuild)(build, newRelease, cluster, options) .then(() => { (0, update_release_status_1.updateReleaseStatusById)(releaseId, "success"); }) .catch((e) => { (0, update_release_status_1.updateReleaseStatusById)(releaseId, "failed"); }); } else { try { await (0, exports.processDeployBuild)(build, newRelease, cluster, options); await (0, update_release_status_1.updateReleaseStatusById)(releaseId, "success"); } catch (e) { await (0, update_release_status_1.updateReleaseStatusById)(releaseId, "failed"); } } return { app: updatedApp, build, release: newRelease, deployment, endpoint, prerelease: null }; }; exports.deployBuild = deployBuild; const deployWithBuildSlug = async (buildSlug, options) => { const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); const build = await DB.findOne("build", { slug: buildSlug }); if (!build) throw new Error(`[DEPLOY BUILD] Build slug "${buildSlug}" not found.`); return (0, exports.deployBuild)(build, options); }; exports.deployWithBuildSlug = deployWithBuildSlug;