UNPKG

@topgroup/diginext

Version:

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

492 lines (491 loc) 25.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.deployWithBuildSlugV2 = exports.deployBuildV2 = exports.processDeployBuildV2 = exports.DeployBuildError = 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 deploy_rollout_v3_1 = require("./deploy-rollout-v3"); const generate_deployment_name_1 = __importDefault(require("./generate-deployment-name")); const generate_deployment_v2_1 = require("./generate-deployment-v2"); class DeployBuildError extends Error { constructor(data, message) { super(message); this.data = data; this.name = "DeployBuildError"; } } exports.DeployBuildError = DeployBuildError; const processDeployBuildV2 = 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); // workspace const { DB } = await Promise.resolve().then(() => __importStar(require("../../modules/api/DB"))); const workspace = await DB.findOne("workspace", { _id: build.workspace }); // app const appSvc = new services_1.AppService({ owner, workspace }); // webhook const webhookSvc = new services_1.WebhookService(); webhookSvc.ownership = { owner, workspace }; const webhook = await DB.findOne("webhook", { release: releaseId }); // mark build & release as "failed" status const markBuildAndReleaseAsFailed = async () => { // update build const _build = await DB.update("build", { _id: build._id }, { deployStatus: "failed" }, { select: ["_id", "status", "deployStatus"], }).catch(console.error); // update release const _release = await DB.update("release", { _id: release._id }, { status: "failed" }, { select: ["_id", "status", "buildStatus"], }).catch(console.error); console.log("markBuildAndReleaseAsFailed() > _build :>> ", _build); console.log("markBuildAndReleaseAsFailed() > _release :>> ", _release); }; // stop deployment if release is undefined if (!release) { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed().catch(console.error); const msg = `❌ Unable to find release for build "${buildTag}".`; (0, build_1.sendLog)({ SOCKET_ROOM, message: msg, type: "error", action: "end" }); throw new DeployBuildError({ build, release, cluster }, msg); } // 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) { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed(); (0, build_1.sendLog)({ SOCKET_ROOM, message: `❌ Unable to connect the cluster: ${e.message}`, type: "error", action: "end" }); throw new DeployBuildError({ build, release, cluster }, 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)) { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed(); (0, build_1.sendLog)({ SOCKET_ROOM, message: `❌ Unable to connect cluster to get namespace list.`, type: "error", action: "end" }); throw new DeployBuildError({ build, release, cluster }, `Unable to connect cluster to get namespace list.`); } if (!isNsExisted) { const createNsResult = await k8s_1.default.createNamespace(namespace, { context }); if (!createNsResult) throw new DeployBuildError({ build, release, cluster }, `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) { const errorMsg = `${e}`; if (errorMsg.indexOf("already exists") > -1) { (0, build_1.sendLog)({ SOCKET_ROOM, type: "warn", action: "log", message: `Can't create "imagePullSecrets" in the "${namespace}" namespace: ${e}`, }); } else { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed(); (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 DeployBuildError({ build, release, cluster }, `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 }); if (!allIngresses) throw new DeployBuildError({ build, release, cluster }, `Found no ingresses in "${context}" cluster.`); let namespaceOfExistingIngress; const ingInAnotherNamespace = allIngresses.find((ing) => { var _a, _b; const findCondition = typeof ((_b = (_a = ing.spec) === null || _a === void 0 ? void 0 : _a.rules) === null || _b === void 0 ? void 0 : _b.find((rule) => rule.host === endpoint)) !== "undefined" && ing.metadata.namespace !== namespace; if (findCondition) namespaceOfExistingIngress = ing.metadata.namespace; return findCondition; }); if (ingInAnotherNamespace) { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed(); 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 DeployBuildError({ build, release, cluster }, message); } } catch (e) { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed(); const message = `[DEPLOY BUILD] Checking ingress > error :>>\n${e.stack}`; (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 DeployBuildError({ build, release, cluster }, 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)) { // update "deployStatus" in a build & a release await markBuildAndReleaseAsFailed(); (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 DeployBuildError({ build, release, cluster }, `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}).`, }); } // NOTE: No need to "deployStatus" in a build & a release below, because there are similar code in "rolloutV2" function // await markBuildAndReleaseAsFailed(); 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 DeployBuildError({ build, release, cluster }, 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 { // ClusterManager.rolloutV2(releaseId, { onUpdate: onRolloutUpdate }); (0, deploy_rollout_v3_1.rolloutV3)(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 DeployBuildError({ build, release, cluster }, errMsg); } } else { if (release._id) { (0, build_1.sendLog)({ SOCKET_ROOM, message: `Rolling out the deployment to "${env.toUpperCase()}" environment...`, }); try { // const result = await ClusterManager.rolloutV2(releaseId, { onUpdate: onRolloutUpdate }); const result = await (0, deploy_rollout_v3_1.rolloutV3)(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 DeployBuildError({ build, release, cluster }, 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 = `[DEPLOY BUILD] Deploy build > Rollout (v3) > error :>>\n${e.stack}`; (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 DeployBuildError({ build, release, cluster }, errMsg); } } } return { build, release, cluster }; }; exports.processDeployBuildV2 = processDeployBuildV2; const deployBuildV2 = async (build, options) => { // parse options const { env, port, owner, workspace, clusterSlug: targetClusterSlug, deployInBackground = true, cliVersion } = options; const { appSlug, projectSlug, tag: buildTag, num: buildNumber, registry: registryId } = 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); // services const userSvc = new services_1.UserService(); const appSvc = new services_1.AppService({ owner, workspace }); const projectSvc = new services_1.ProjectService({ owner, workspace }); const clusterSvc = new services_1.ClusterService({ owner, workspace }); const registrySvc = new services_1.ContainerRegistryService({ owner, workspace }); // update server deploy environment data // app info let app = await appSvc.updateOne({ slug: appSlug }, { [`deployEnvironment.${env}.healthzPath`]: options.healthzPath, 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.`); } // project info const project = app.project; const projectOwner = await userSvc.findOne({ _id: project.owner }); const appOwner = app.owner; // app version const mainAppName = await (0, generate_deployment_name_1.default)(app); const appVersion = `${mainAppName}-${buildNumber}`; // 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 appSvc.findOne({ 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 target cluster is defined, then set it to the deploy environment if (targetClusterSlug) serverDeployEnvironment.cluster = targetClusterSlug; // if no cluster is defined for this deploy environment, throw error 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 clusterSvc.findOne({ slug: clusterSlug }, { subpath: "/all" }); // find registry const registry = registryId ? await registrySvc.findOne({ _id: registryId }) : undefined; // 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, port, username, workspace, buildImage: build.image, buildTag, appConfig, targetDirectory: buildDirectory, registry, }); } 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: errMsg, action: "end" }); throw new Error(errMsg); } const { endpoint, deploymentContent, deployEnvironment, deploymentName } = deployment; // update data to deploy environment: serverDeployEnvironment = { ...serverDeployEnvironment, ...deployEnvironment }; serverDeployEnvironment.deploymentYaml = deploymentContent; serverDeployEnvironment.deploymentName = deploymentName; 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 appSvc.updateOne({ slug: appSlug }, updatedAppData); // console.log("deployBuildV2() > updatedApp :>> "); // console.dir(updatedApp, { depth: 10 }); // console.log("updatedApp.deployEnvironment[env].envVars :>> ", updatedApp.deployEnvironment[env].envVars); // console.log(`deploymentContent :>> `, deploymentContent); // console.log(`updatedApp.deployEnvironment[env].deploymentYaml :>> `, updatedApp.deployEnvironment[env].deploymentYaml); (0, build_1.sendLog)({ SOCKET_ROOM, message: `[DEPLOY BUILD] Generated the deployment files successfully!` }); // log(`[BUILD] App's last updated by "${updatedApp.lastUpdatedBy}".`); // Create new Release: let releaseId, newRelease; try { newRelease = await (0, build_1.createReleaseFromBuild)(build, env, { author: owner, workspace, cliVersion, appVersion }); 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, }); } // update project "lastUpdatedBy" await projectSvc .updateOne({ _id: project._id }, { lastUpdatedBy: username, latestBuild: build._id, }) .catch(console.error); // process deploy build to cluster if (deployInBackground) { (0, exports.processDeployBuildV2)(build, newRelease, cluster, options) .then(({ release }) => { (0, update_release_status_1.updateReleaseStatusById)(mongodb_1.MongoDB.toString(release._id), "success"); }) .catch((e) => { if (e instanceof DeployBuildError) { const { release } = e.data; (0, update_release_status_1.updateReleaseStatusById)(mongodb_1.MongoDB.toString(release._id), "failed"); } }); } else { try { await (0, exports.processDeployBuildV2)(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 }; }; exports.deployBuildV2 = deployBuildV2; const deployWithBuildSlugV2 = 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.deployBuildV2)(build, options); }; exports.deployWithBuildSlugV2 = deployWithBuildSlugV2;