UNPKG

@topgroup/diginext

Version:

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

940 lines 55.4 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.DeployEnvironmentService = void 0; const class_validator_1 = require("class-validator"); const dayjs_1 = __importDefault(require("dayjs")); const Slug_1 = require("diginext-utils/dist/Slug"); const log_1 = require("diginext-utils/dist/xconsole/log"); const lodash_1 = require("lodash"); const const_1 = require("../config/const"); const SystemTypes_1 = require("../interfaces/SystemTypes"); const get_app_environment_1 = require("../modules/apps/get-app-environment"); const create_release_from_app_1 = require("../modules/build/create-release-from-app"); const deploy_1 = require("../modules/deploy"); const generate_deployment_name_1 = __importDefault(require("../modules/deploy/generate-deployment-name")); const generate_deployment_v2_1 = require("../modules/deploy/generate-deployment-v2"); const dx_domain_1 = require("../modules/diginext/dx-domain"); const k8s_1 = __importDefault(require("../modules/k8s")); const check_quota_1 = require("../modules/workspace/check-quota"); const plugins_1 = require("../plugins"); const array_1 = require("../plugins/array"); const env_var_1 = require("../plugins/env-var"); const k8s_helper_1 = require("../plugins/k8s-helper"); const mongodb_1 = require("../plugins/mongodb"); const string_1 = require("../plugins/string"); const server_1 = require("../server"); class DeployEnvironmentService { constructor(ownership) { this.ownership = ownership; this.user = ownership === null || ownership === void 0 ? void 0 : ownership.owner; this.workspace = ownership === null || ownership === void 0 ? void 0 : ownership.workspace; } async createDeployEnvironment(appSlug, params, ownership) { // conversion if needed... if ((0, class_validator_1.isJSON)(params.deployEnvironmentData)) params.deployEnvironmentData = JSON.parse(params.deployEnvironmentData); // const { env, deployEnvironmentData } = params; if (!appSlug) throw new Error(`App slug is required.`); if (!env) throw new Error(`Deploy environment name is required.`); if (!deployEnvironmentData) throw new Error(`Deploy environment configuration is required.`); const { AppService, ClusterService, ContainerRegistryService, WorkspaceService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(); const clusterSvc = new ClusterService(); const regSvc = new ContainerRegistryService(); // get app data: const app = await appSvc.findOne({ slug: appSlug }, { populate: ["project"] }); if (!app) if (ownership === null || ownership === void 0 ? void 0 : ownership.owner) throw new Error(`Unauthorized.`); else throw new Error(`App not found.`); if (!app.project) throw new Error(`This app is orphan, apps should belong to a project.`); if (!deployEnvironmentData.imageURL) throw new Error(`Build image URL is required.`); if (!deployEnvironmentData.buildTag) throw new Error(`Build number (image's tag) is required.`); // workspace const wsId = app.workspace ? (mongodb_1.MongoDB.isValidObjectId(app.workspace) ? app.workspace : app.workspace._id) : undefined; if (!wsId) throw new Error(`Workspace ID is not valid.`); const wsSvc = new WorkspaceService(); const workspace = this.workspace || ownership.workspace || (await wsSvc.findOne({ _id: wsId })); if (!workspace) throw new Error(`Workspace not found.`); // build const { buildTag } = deployEnvironmentData; // project const project = app.project; const { slug: projectSlug } = project; // DEPLOYMENT: Assign default values to optional params: const mainAppName = await (0, generate_deployment_name_1.default)(app); const deprecatedMainAppName = (0, Slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); if (!deployEnvironmentData.size) deployEnvironmentData.size = "1x"; if (!deployEnvironmentData.shouldInherit) deployEnvironmentData.shouldInherit = true; if (!deployEnvironmentData.replicas) deployEnvironmentData.replicas = 1; if (!deployEnvironmentData.redirect) deployEnvironmentData.redirect = true; // Check DX quota const quotaRes = await (0, check_quota_1.checkQuota)(workspace, { resourceSize: deployEnvironmentData.size }); if (!quotaRes.status) throw new Error(quotaRes.messages.join(". ")); if (quotaRes.data && quotaRes.data.isExceed) throw new Error(`You've exceeded the limit amount of container size (${quotaRes.data.type} / Max size: ${quotaRes.data.limits.size}x).`); // Validate deploy environment data: // cluster if (!deployEnvironmentData.cluster) throw new Error(`Param "cluster" (Cluster's short name) is required.`); const cluster = await clusterSvc.findOne({ slug: deployEnvironmentData.cluster }); if (!cluster) throw new Error(`Cluster "${deployEnvironmentData.cluster}" is not valid`); // namespace if (!deployEnvironmentData.namespace) deployEnvironmentData.namespace = `${projectSlug}-${env}`; // container registry if (!deployEnvironmentData.registry) throw new Error(`Param "registry" (Container Registry's slug) is required.`); const registry = await regSvc.findOne({ slug: deployEnvironmentData.registry }); if (!registry) throw new Error(`Container Registry "${deployEnvironmentData.registry}" is not existed.`); // Domains & SSL certificate... if (!deployEnvironmentData.domains) deployEnvironmentData.domains = []; if (deployEnvironmentData.useGeneratedDomain) { const subdomain = `${projectSlug}-${appSlug}.${env}`; const { status, messages, data: { domain }, } = await (0, dx_domain_1.dxCreateDomain)({ name: subdomain, data: cluster.primaryIP, userId: this.user.dxUserId }, ownership.workspace.dx_key); if (!status) (0, log_1.logWarn)(`[APP_CONTROLLER] ${messages.join(". ")}`); deployEnvironmentData.domains = status ? [domain, ...deployEnvironmentData.domains] : deployEnvironmentData.domains; } if (!deployEnvironmentData.ssl) { deployEnvironmentData.ssl = deployEnvironmentData.domains.length > 0 ? "letsencrypt" : "none"; } if (!SystemTypes_1.sslIssuerList.includes(deployEnvironmentData.ssl)) throw new Error(`Param "ssl" issuer is invalid, should be one of: "letsencrypt", "custom" or "none".`); if (deployEnvironmentData.ssl === "letsencrypt") { deployEnvironmentData.tlsSecret = (0, Slug_1.makeSlug)(deployEnvironmentData.domains[0]); } else if (deployEnvironmentData.ssl === "custom") { if (!deployEnvironmentData.tlsSecret) { deployEnvironmentData.tlsSecret = (0, Slug_1.makeSlug)(deployEnvironmentData.domains[0]); } } else { deployEnvironmentData.tlsSecret = ""; } // Exposing ports, enable/disable CDN, and select Ingress type if ((0, lodash_1.isUndefined)(deployEnvironmentData.port)) throw new Error(`Param "port" is required.`); if ((0, lodash_1.isUndefined)(deployEnvironmentData.cdn) || !(0, lodash_1.isBoolean)(deployEnvironmentData.cdn)) deployEnvironmentData.cdn = false; // deployEnvironmentData.ingress = "nginx"; // create deploy environment in the app: let updatedApp = await appSvc.updateOne({ slug: appSlug }, { [`deployEnvironment.${env}`]: deployEnvironmentData, }); // console.log("updatedApp :>> ", updatedApp); if (!updatedApp) throw new Error(`Failed to create "${env}" deploy environment.`); // const appConfig = await getAppConfigFromApp(updatedApp); // console.log("buildTag :>> ", buildTag); let deployment = await (0, generate_deployment_v2_1.generateDeploymentV2)({ env, skipPrerelease: true, appSlug: app.slug, username: ownership.owner.slug, workspace: ownership.workspace, buildTag: buildTag, }); const { endpoint, deploymentContent } = deployment; // update data to deploy environment: let serverDeployEnvironment = await (0, get_app_environment_1.getDeployEvironmentByApp)(updatedApp, env); // serverDeployEnvironment.prereleaseUrl = prereleaseUrl; serverDeployEnvironment.deploymentYaml = deploymentContent; // serverDeployEnvironment.prereleaseDeploymentYaml = prereleaseDeploymentContent; serverDeployEnvironment.updatedAt = new Date(); serverDeployEnvironment.lastUpdatedBy = ownership.owner.username; serverDeployEnvironment.owner = mongodb_1.MongoDB.toString(this.user._id); serverDeployEnvironment.ownerSlug = this.user.slug; // Update {user}, {project}, {environment} to database before rolling out const updatedAppData = { deployEnvironment: updatedApp.deployEnvironment || {} }; updatedAppData.lastUpdatedBy = ownership.owner.username; updatedAppData.deployEnvironment[env] = serverDeployEnvironment; updatedApp = await appSvc.updateOne({ slug: app.slug }, updatedAppData); if (!updatedApp) throw new Error("Unable to apply new domain configuration for " + env + " environment of " + app.slug + "app."); // ----- SHOULD ROLL OUT NEW RELEASE OR NOT ---- let workloads = await k8s_1.default.getDeploysByFilter(serverDeployEnvironment.namespace, { context: cluster.contextName, filterLabel: `main-app=${mainAppName}`, }); // Fallback support for deprecated mainAppName if (!workloads || workloads.length === 0) { workloads = await k8s_1.default.getDeploysByFilter(serverDeployEnvironment.namespace, { context: cluster.contextName, filterLabel: `main-app=${deprecatedMainAppName}`, }); } if (workloads && workloads.length > 0) { // create new release and roll out const release = await (0, create_release_from_app_1.createReleaseFromApp)(updatedApp, env, buildTag, { author: ownership.owner, cliVersion: (0, plugins_1.currentVersion)(), workspace: ownership.workspace, }); const result = await k8s_1.default.rollout(release._id.toString()); if (result.error) throw new Error(`Failed to roll out the release :>> ${result.error}.`); } return updatedApp; } async getDeployEnvironmentStatus(deployEnvironment) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o; const { ClusterService } = await Promise.resolve().then(() => __importStar(require("./index"))); const clusterSvc = new ClusterService(); const { app, name: env } = deployEnvironment; if (!app) throw new Error(`App not found.`); if (!deployEnvironment.buildTag) deployEnvironment.buildTag = ""; // format environment variables (if any) if (deployEnvironment.envVars) { deployEnvironment.envVars = (0, env_var_1.formatEnvVars)(deployEnvironment.envVars); } // default values deployEnvironment.readyCount = 0; deployEnvironment.status = "undeployed"; // if no cluster -> not deployed -> skip if (!deployEnvironment.cluster) return deployEnvironment; // find cluster const clusterSlug = deployEnvironment.cluster; const cluster = await clusterSvc.findOne({ slug: clusterSlug }, { subpath: "/all" }).catch(() => null); if (!cluster) return deployEnvironment; // find context & namespace const { contextName: context } = cluster; if (!context) return deployEnvironment; const { namespace } = deployEnvironment; if (!namespace) return deployEnvironment; // find workloads base on "main-app" label const mainAppName = await (0, generate_deployment_name_1.default)(app); let [deployOnCluster] = await k8s_1.default.getDeploys(namespace, { filterLabel: `main-app=${mainAppName}`, context, metrics: true, }); console.log(`----- ${app.name} -----`); // console.log("- mainAppName :>> ", mainAppName); // console.log("- deployOnCluster.metadata.name :>> ", deployOnCluster?.metadata?.name); console.log("- deployOnCluster.status.replicas :>> ", (_a = deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.status) === null || _a === void 0 ? void 0 : _a.replicas); console.log("- deployOnCluster.resources.limits :>> ", (_f = (_e = (_d = (_c = (_b = deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.spec) === null || _b === void 0 ? void 0 : _b.template) === null || _c === void 0 ? void 0 : _c.spec) === null || _d === void 0 ? void 0 : _d.containers) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.resources.limits); console.log("- deployOnCluster.cpuAvg :>> ", deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.cpuAvg); console.log("- deployOnCluster.memoryAvg :>> ", deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.memoryAvg); // console.log("- deployOnCluster.status.readyReplicas :>> ", deployOnCluster?.status?.readyReplicas); // console.log("- deployOnCluster.status.availableReplicas :>> ", deployOnCluster?.status?.availableReplicas); // console.log("- deployOnCluster.status.unavailableReplicas :>> ", deployOnCluster?.status?.unavailableReplicas); deployEnvironment.resources = { limits: (_l = (_k = (_j = (_h = (_g = deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.spec) === null || _g === void 0 ? void 0 : _g.template) === null || _h === void 0 ? void 0 : _h.spec) === null || _j === void 0 ? void 0 : _j.containers) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.resources.limits, usage: { cpu: deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.cpuAvg, memory: deployOnCluster === null || deployOnCluster === void 0 ? void 0 : deployOnCluster.memoryAvg, }, }; if (!deployOnCluster) { deployEnvironment.status = "undeployed"; return deployEnvironment; } deployEnvironment.readyCount = (_o = (_m = deployOnCluster.status.readyReplicas) !== null && _m !== void 0 ? _m : deployOnCluster.status.availableReplicas) !== null && _o !== void 0 ? _o : 0; if (deployOnCluster.status.replicas === deployOnCluster.status.availableReplicas || deployOnCluster.status.replicas === deployOnCluster.status.readyReplicas) { deployEnvironment.status = "healthy"; return deployEnvironment; } if (deployOnCluster.status.unavailableReplicas && deployOnCluster.status.unavailableReplicas > 0) { deployEnvironment.status = "partial_healthy"; return deployEnvironment; } if (deployOnCluster.status.availableReplicas === 0 || deployOnCluster.status.unavailableReplicas === deployOnCluster.status.replicas || deployOnCluster.status.readyReplicas === 0) { deployEnvironment.status = "failed"; return deployEnvironment; } deployEnvironment.status = "unknown"; return deployEnvironment; } async getAllDeployEnvironments(workspaceId, options) { if (!workspaceId) throw new Error(`Workspace ID is required.`); // try to get from redis const redisKey = `listDeployEnvironments:${workspaceId}`; if (typeof (options === null || options === void 0 ? void 0 : options.cache) === "undefined" || options.cache) { try { const redisRes = await server_1.redis.get(redisKey); if (redisRes) return JSON.parse(redisRes); } catch (e) { (0, log_1.logWarn)(`[DEPLOY_ENVIRONMENT_SERVICE] Unable to get getAllDeployEnvironments from redis: ${e}`); } } try { const { AppService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(); const apps = await appSvc.find({ workspace: this.workspace._id }, { populate: ["project"] }); let deployEnvironments = []; apps.forEach((app) => { if (app.deployEnvironment) { Object.entries(app.deployEnvironment).forEach(([env, deployEnvironment]) => { if (deployEnvironment) { // Create a new object to avoid circular references const safeDeployEnvironment = { ...deployEnvironment, env, deploymentName: deployEnvironment.deploymentName || (deployEnvironment.deploymentYaml ? (0, deploy_1.fetchDeploymentFromContent)(deployEnvironment.deploymentYaml).APP_NAME : undefined), appSlug: app.slug, appName: app.name, projectName: app.project ? app.project.name : undefined, projectSlug: app.project ? app.project.slug : undefined, app: { id: app._id, slug: app.slug, name: app.name, owner: app.owner, workspace: app.workspace, project: app.project, }, updatedAt: deployEnvironment.updatedAt ? (0, dayjs_1.default)(deployEnvironment.updatedAt).toDate() : (0, dayjs_1.default)().subtract(1, "year").toDate(), }; // Remove circular references // delete safeDeployEnvironment.app; deployEnvironments.push(safeDeployEnvironment); } }); } }); // sort by updatedAt (descending) deployEnvironments.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); // save to redis (expire in 1 hour) await server_1.redis.set(redisKey, JSON.stringify(deployEnvironments), "EX", 60 * 60); return deployEnvironments; } catch (e) { (0, log_1.logWarn)(`[DEPLOY_ENVIRONMENT_SERVICE] Unable to get getAllDeployEnvironments from redis: ${e}`); return []; } } async listDeployEnvironments(filter, options) { if (typeof (options === null || options === void 0 ? void 0 : options.cache) === "undefined") options.cache = true; options.cache = false; const redisKey = `listDeployEnvironments:${JSON.stringify(filter)}:${JSON.stringify(options)}`; if (options.cache) { try { const redisRes = await server_1.redis.get(redisKey); if (redisRes) { const deployEnvironments = redisRes ? JSON.parse(redisRes) : []; // sort by updatedAt (descending) deployEnvironments.sort((a, b) => ((0, dayjs_1.default)(b.updatedAt).toDate() || (0, dayjs_1.default)().subtract(1, "year").toDate()).getTime() - ((0, dayjs_1.default)(a.updatedAt).toDate() || (0, dayjs_1.default)().subtract(1, "year").toDate()).getTime()); // pagination (optional) const pagination = { skip: (options === null || options === void 0 ? void 0 : options.skip) || 0, limit: (options === null || options === void 0 ? void 0 : options.limit) || 50, total: deployEnvironments.length, page: Math.ceil(deployEnvironments.length / (options === null || options === void 0 ? void 0 : options.limit)), size: options === null || options === void 0 ? void 0 : options.limit, }; return { data: deployEnvironments, pagination }; } } catch (e) { (0, log_1.logWarn)(`[DEPLOY_ENVIRONMENT_SERVICE] Unable to get listDeployEnvironments from redis: ${e}`); } } // extract deploy environments from apps let deployEnvironments = await this.getAllDeployEnvironments(this.workspace._id.toString(), { cache: true }); if (filter === null || filter === void 0 ? void 0 : filter.workspace) delete filter.workspace; console.log(`filter :>> `, filter); // filter deploy environments if (filter) { deployEnvironments = deployEnvironments.filter((deployEnvironment) => { // Dynamically filter based on the provided filter object return Object.entries(filter).every(([key, value]) => { // Check if the key exists in the deployEnvironment and matches the filter value return value ? deployEnvironment[key] === value : true; }); }); } console.log(`[2] deployEnvironments :>> `, deployEnvironments.length); // pagination (optional) const pagination = { skip: (options === null || options === void 0 ? void 0 : options.skip) || 0, limit: (options === null || options === void 0 ? void 0 : options.limit) || 50, total: deployEnvironments.length, page: Math.ceil(deployEnvironments.length / (options === null || options === void 0 ? void 0 : options.limit)), size: options === null || options === void 0 ? void 0 : options.limit, }; deployEnvironments = deployEnvironments.slice(options.skip, options.skip + options.limit); console.log(`[3] deployEnvironments :>> `, deployEnvironments.length); // get status of each deploy environment if (options === null || options === void 0 ? void 0 : options.status) { deployEnvironments = await Promise.all(deployEnvironments.map(async (deployEnvironment) => { // Find the original app to pass to getDeployEnvironmentStatus // const originalApp = apps.find((app) => app.slug === deployEnvironment.appSlug); return this.getDeployEnvironmentStatus({ ...deployEnvironment, // app: originalApp, }); })); } console.log(`[4] deployEnvironments :>> `, deployEnvironments.length); // save to redis (expire in 1 hour) await server_1.redis.set(redisKey, JSON.stringify(deployEnvironments), "EX", 60 * 60).catch((e) => { (0, log_1.logWarn)(`[DEPLOY_ENVIRONMENT_SERVICE] Unable to save listDeployEnvironments to redis: ${e}`); }); return { data: deployEnvironments, pagination }; } async viewDeployEnvironmentLogs(app, env) { const deployEnvironment = app.deployEnvironment[env]; const { ClusterService } = await Promise.resolve().then(() => __importStar(require("./index"))); const clusterSvc = new ClusterService(); const clusterSlug = deployEnvironment.cluster; if (!clusterSlug) throw new Error(`Cluster slug not found.`); const [cluster] = await clusterSvc.findAll({ slug: clusterSlug, workspace: app.workspace }); if (!cluster) throw new Error(`Cluster "${clusterSlug}" not found.`); const { contextName: context } = cluster; const mainAppName = await (0, generate_deployment_name_1.default)(app); const pods = await k8s_1.default.getPodsByFilter(deployEnvironment.namespace, { context, filterLabel: `main-app=${mainAppName}`, metrics: false, }); const deprecatedMainAppName = (0, Slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); const deprecatedApps = await k8s_1.default.getPodsByFilter(deployEnvironment.namespace, { context, filterLabel: `main-app=${deprecatedMainAppName}`, metrics: false, }); pods.push(...deprecatedApps); // console.log("pods :>> ", pods); if ((0, lodash_1.isEmpty)(pods)) return; const logs = {}; await Promise.all(pods.map(async (pod) => { // console.log("pod.metadata :>> ", pod.metadata); const podLogs = await k8s_1.default.logPod(pod.metadata.name, deployEnvironment.namespace, { context }); logs[pod.metadata.name] = podLogs; return podLogs; })); // console.log("logs :>> ", logs); return logs; } /** * Make deploy environment sleep by scale the replicas to ZERO, so you can wake it up later without re-deploy. */ async sleepDeployEnvironment(app, env) { if (!env) throw new Error(`Params "env" (deploy environment) is required.`); const deployEnvironment = app.deployEnvironment[env]; if (!deployEnvironment) throw new Error(`Deploy environment "${env}" not found.`); const clusterSlug = deployEnvironment.cluster; if (!clusterSlug) throw new Error(`This app's deploy environment (${env}) hasn't been deployed in any clusters.`); const { AppService, ClusterService, ContainerRegistryService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(); const clusterSvc = new ClusterService(); const cluster = await clusterSvc.findOne({ slug: clusterSlug }); if (!cluster) throw new Error(`Cluster "${clusterSlug}" not found.`); if (!deployEnvironment.namespace) throw new Error(`Namespace not found.`); const { contextName: context } = cluster; const { namespace } = deployEnvironment; // get deployment's labels const mainAppName = await (0, generate_deployment_name_1.default)(app); const deprecatedMainAppName = (0, Slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); // switch to the cluster of this environment await k8s_1.default.authCluster(cluster, { ownership: this.ownership }); let success = false; let message = ""; try { /** * FALLBACK SUPPORT for deprecated mainAppName */ await k8s_1.default.scaleDeployByFilter(0, namespace, { context, filterLabel: `main-app=${deprecatedMainAppName}` }); success = true; } catch (e) { // skip... } try { await k8s_1.default.scaleDeployByFilter(0, namespace, { context, filterLabel: `main-app=${mainAppName}` }); success = true; } catch (e) { message = `Unable to sleep a deploy environment "${env}" on cluster: ${clusterSlug} (Namespace: ${namespace}): ${e}`; } // update database appSvc.updateOne({ _id: app._id }, { [`deployEnvironment.${env}.replicas`]: 0, [`deployEnvironment.${env}.sleepAt`]: new Date(), }); return { success, message }; } /** * Wake a sleeping deploy environment up by scale it to 1 (Will FAIL if this environment hasn't been deployed). */ async wakeUpDeployEnvironment(app, env) { if (!env) throw new Error(`Params "env" (deploy environment) is required.`); const deployEnvironment = app.deployEnvironment[env]; if (!deployEnvironment) throw new Error(`Deploy environment "${env}" not found.`); const clusterSlug = deployEnvironment.cluster; if (!clusterSlug) throw new Error(`This app's deploy environment (${env}) hasn't been deployed in any clusters.`); const { AppService, ClusterService, ContainerRegistryService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(); const clusterSvc = new ClusterService(); const cluster = await clusterSvc.findOne({ slug: clusterSlug }); if (!cluster) throw new Error(`Cluster "${clusterSlug}" not found.`); if (!deployEnvironment.namespace) throw new Error(`Namespace not found.`); const { contextName: context } = cluster; const { namespace } = deployEnvironment; // get deployment's labels const mainAppName = await (0, generate_deployment_name_1.default)(app); const deprecatedMainAppName = (0, Slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); // switch to the cluster of this environment await k8s_1.default.authCluster(cluster, { ownership: this.ownership }); let success = false; let message = ""; try { /** * FALLBACK SUPPORT for deprecated mainAppName */ await k8s_1.default.scaleDeployByFilter(1, namespace, { context, filterLabel: `main-app=${deprecatedMainAppName}` }); success = true; } catch (e) { // skip... } try { await k8s_1.default.scaleDeployByFilter(1, namespace, { context, filterLabel: `main-app=${mainAppName}` }); success = true; } catch (e) { message = `Unable to wake up a deploy environment "${env}" on cluster: ${clusterSlug} (Namespace: ${namespace}): ${e}`; } // update database appSvc.updateOne({ _id: app._id }, { [`deployEnvironment.${env}.replicas`]: 1, [`deployEnvironment.${env}.awakeAt`]: new Date(), }); return { success, message }; } /** * Take down a deploy environment but still keep the deploy environment information (cluster, registry, namespace,...) */ async takeDownDeployEnvironment(app, env, options) { if (!app.deployEnvironment) throw new Error(`Unable to take down, this app doesn't have any deploy environments.`); if (!env) throw new Error(`Unable to take down, param "env" (deploy environment) is required.`); let errorMsg; const deployEnvironment = app.deployEnvironment[env]; if (!deployEnvironment) throw new Error(`Deploy environment "${env}" not found.`); const clusterSlug = deployEnvironment.cluster; if (!clusterSlug) throw new Error(`This app's deploy environment (${env}) hasn't been deployed in any clusters.`); const { AppService, ClusterService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(); const clusterSvc = new ClusterService(); const cluster = await clusterSvc.findOne({ slug: clusterSlug }); if (!cluster) { console.log(`[DEPLOY_ENVIRONMENT] takeDownDeployEnvironment() > Cluster "${clusterSlug}" not found.`); // response data errorMsg = `Cluster "${clusterSlug}" not found.`; return { app: { slug: app.slug, id: app._id, owner: app.owner, workspace: app.workspace, }, success: true, message: errorMsg, }; } if (!deployEnvironment.namespace) { console.log(`[DEPLOY_ENVIRONMENT] takeDownDeployEnvironment() > Namespace not found.`); errorMsg = `Namespace not found.`; return { app: { slug: app.slug, id: app._id, owner: app.owner, workspace: app.workspace, }, success: true, message: errorMsg, }; } const { contextName: context } = cluster; const { namespace } = deployEnvironment; // TODO: get "main-app" label in the "release" of this app // get deployment's labels const mainAppName = await (0, generate_deployment_name_1.default)(app); const deprecatedMainAppName = (0, Slug_1.makeSlug)(app === null || app === void 0 ? void 0 : app.name).toLowerCase(); // double check cluster's accessibility await k8s_1.default.authCluster(cluster, { ownership: this.ownership }); /** * IMPORTANT * --- * Should NOT delete namespace because it will affect other apps in a project! */ try { // Delete INGRESS await k8s_1.default.deleteIngressByFilter(namespace, { context, filterLabel: `main-app=${mainAppName}` }); // Delete SERVICE await k8s_1.default.deleteServiceByFilter(namespace, { context, filterLabel: `main-app=${mainAppName}` }); // Delete DEPLOYMENT await k8s_1.default.deleteDeploymentsByFilter(namespace, { context, filterLabel: `main-app=${mainAppName}` }); console.log(`✅ Deleted "${mainAppName}" deployment.`); } catch (e) { errorMsg = `Unable to delete deploy environment "${env}" on cluster: ${clusterSlug} (Namespace: ${namespace}): ${e}`; } try { /** * FALLBACK SUPPORT for deprecated mainAppName */ // Delete INGRESS await k8s_1.default.deleteIngressByFilter(namespace, { context, filterLabel: `main-app=${deprecatedMainAppName}` }); // Delete SERVICE await k8s_1.default.deleteServiceByFilter(namespace, { context, filterLabel: `main-app=${deprecatedMainAppName}` }); // Delete DEPLOYMENT await k8s_1.default.deleteDeploymentsByFilter(namespace, { context, filterLabel: `main-app=${deprecatedMainAppName}` }); console.log(`✅ Deleted "${deprecatedMainAppName}" deployment.`); } catch (e) { errorMsg += `, ${e}.`; } if (options === null || options === void 0 ? void 0 : options.isDebugging) console.error(`[DEPLOY_ENV_SERVICE]`, errorMsg); // update database const tookDownAt = new Date(); app = await appSvc.updateOne({ _id: app._id }, { [`deployEnvironment.${env}.tookDownAt`]: tookDownAt }); // response data return { app: { slug: app.slug, id: app._id, owner: app.owner, workspace: app.workspace, tookDownAt, cluster: cluster.slug, }, success: true, message: errorMsg, }; } async deleteDeployEnvironment(app, env) { const { AppService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(); // take down deploy environment on clusters await this.takeDownDeployEnvironment(app, env); // delete DXUP domain record (if any) const deployEnvironment = app.deployEnvironment[env]; if (deployEnvironment.domains && deployEnvironment.domains.filter((domain) => domain.indexOf(const_1.DIGINEXT_DOMAIN) > -1).length > 0) { if (this.workspace && this.workspace.dx_key) { for (const domain of deployEnvironment.domains.filter((_domain) => _domain.indexOf(const_1.DIGINEXT_DOMAIN) > -1)) { const recordName = domain.replace(const_1.DIGINEXT_DOMAIN, ""); (0, dx_domain_1.dxDeleteDomainRecord)({ name: recordName, type: "A" }, this.workspace.dx_key).catch(console.error); } } else { console.error("DeployEnvironmentService > deleteDeployEnvironment() > Delete domain A record > No WORKSPACE or DX_KEY found."); } } // delete deploy environment in database const updatedApp = await appSvc.updateOne({ _id: app._id, }, { $unset: { [`deployEnvironment.${env}`]: true }, }, { raw: true }); return updatedApp; } /** * Change cluster of a deploy environment * @param app * @param env * @param cluster * @param options * @returns */ async changeCluster(app, env, cluster, options) { // validate const deployEnvironment = app.deployEnvironment[env]; if (!deployEnvironment) throw new Error(`Deploy environment "${env}" not found.`); if (!deployEnvironment.buildId) throw new Error(`This deploy environment (${env}) hasn't been built before.`); if (!deployEnvironment.cluster || !deployEnvironment.latestRelease) throw new Error(`This deploy environment (${env}) hasn't been deployed to any clusters.`); // verify target cluster if (!cluster.isVerified || !cluster.contextName) throw new Error(`Cluster "${cluster.slug}" hasn't been verified.`); // delete app on previous cluster (if needed) if (options.deleteAppOnPreviousCluster) { app = await this.deleteDeployEnvironment(app, env); } const { AppService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(this.ownership); // update new cluster slug: app = await appSvc.updateOne({ _id: app._id }, { [`deployEnvironment.${env}.cluster`]: cluster.slug }); // get latest release const { ReleaseService } = await Promise.resolve().then(() => __importStar(require("./index"))); const releaseSvc = new ReleaseService(this.ownership); const release = await releaseSvc.findOne({ _id: deployEnvironment.latestRelease }, { populate: ["build"] }); if (!release.build) throw new Error(`Build not found in this release.`); const build = release.build; // clone to new release with new cluster slug: const newReleaseData = { ...release, cluster: cluster.slug }; delete newReleaseData._id; delete newReleaseData.id; delete newReleaseData.slug; delete newReleaseData.createdAt; delete newReleaseData.updatedAt; delete newReleaseData.owner; delete newReleaseData.ownerSlug; const newRelease = await releaseSvc.create(newReleaseData); if (!newRelease) throw new Error(`Unable to create new release.`); // roll out new release: const rolloutResult = await k8s_1.default.rolloutV2(mongodb_1.MongoDB.toString(newRelease._id)); if (rolloutResult.error) throw new Error(rolloutResult.error); // change DXUP domain record (if any) if (deployEnvironment.domains && deployEnvironment.domains.filter((domain) => domain.indexOf(".diginext.site") > -1).length > 0) { if (this.workspace && this.workspace.dx_key) { for (const domain of deployEnvironment.domains.filter((_domain) => _domain.indexOf(".diginext.site") > -1)) { const recordName = domain.replace(".diginext.site", ""); (0, dx_domain_1.dxUpdateDomainRecord)({ name: recordName, type: "A" }, { data: cluster.primaryIP, userId: this.user.dxUserId }, this.workspace.dx_key).catch(console.error); } } else { console.error("DeployEnvironmentService > changeCluster() > Update domain A record data > No WORKSPACE or DX_KEY found."); } } // return return { build, release: newRelease, app }; } /** * Get environment variables of a deploy environment * @param app - IApp * @param env - Deploy environment (dev, prod,...) * @returns */ async getEnvVars(app, env) { // validate if (!env) throw new Error(`Params "env" (deploy environment) is required.`); return (0, env_var_1.formatEnvVars)(app.deployEnvironment[env].envVars); } /** * Update environment variables of a deploy environment * @param app - IApp * @param env - Deploy environment (dev, prod,...) * @param variables - Array of environment variables: `[{name,value}]` * @returns */ async updateEnvVars(app, env, variables) { // validate if (!env) throw new Error(`Params "env" (deploy environment) is required.`); if (!variables) throw new Error(`Params "variables" (array of environment variables) is required.`); if (!(0, lodash_1.isArray)(variables)) throw new Error(`Params "variables" should be an array.`); // just to make sure "value" is always "string" variables = (0, env_var_1.formatEnvVars)(variables); const deployEnvironment = app.deployEnvironment[env]; if (!deployEnvironment) throw new Error(`Deploy environment "${env}" not found.`); const { AppService, ClusterService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(this.ownership); const appSlug = app.slug; // process let updatedApp = await appSvc.updateOne({ _id: app._id }, { [`deployEnvironment.${env}.envVars`]: variables, [`deployEnvironment.${env}.lastUpdatedBy`]: this.user.slug, [`deployEnvironment.${env}.updatedAt`]: new Date(), }); if (!updatedApp) throw new Error(`Unable to update variables of "${env}" deploy environment (App: "${appSlug}").`); // TO BE REMOVED SOON: Fallback support "buildNumber" if (!deployEnvironment.buildTag && deployEnvironment.buildNumber) deployEnvironment.buildTag = deployEnvironment.buildNumber; console.log("deployEnvironment.buildTag :>> ", deployEnvironment.buildTag); let message = ""; // update on cluster -> if it's failed, just ignore and return warning message! if (deployEnvironment.cluster && deployEnvironment.buildTag) { console.log("Applying new env vars.."); try { const clusterSlug = deployEnvironment.cluster; const clusterSvc = new ClusterService(this.ownership); const cluster = await clusterSvc.findOne({ slug: clusterSlug }); if (!cluster) throw new Error(`Cluster "${clusterSlug}" not found.`); const { contextName: context } = cluster; const { buildTag } = deployEnvironment; // generate new deployment YAML let deployment = await (0, generate_deployment_v2_1.generateDeploymentV2)({ env, skipPrerelease: true, appSlug, buildTag, username: this.user.slug, workspace: this.workspace, }); // apply deployment YAML await k8s_1.default.kubectlApplyContent(deployment.deploymentContent, { context }); // update to database updatedApp = await appSvc.updateOne({ _id: app._id }, { [`deployEnvironment.${env}.deploymentYaml`]: deployment.deploymentContent, // [`deployEnvironment.${env}.prereleaseDeploymentYaml`]: deployment.prereleaseDeploymentContent, }); } catch (e) { message = e.toString(); } } return { app: updatedApp, message }; } /** * Add persistent volume to deploy environment * @param app - IApp * @param env - Deploy environment (dev, prod,...) * @param data - Persistent volume configuration */ async addPersistentVolume(app, env, data) { var _a; // validate if (!app) throw new Error(`App's data is required`); if (!data) throw new Error(`Volume configuration data is required`); if ((0, string_1.containsSpecialCharacters)(data.name)) throw new Error(`Volume name cannot contain any special characters.`); if (!app.deployEnvironment) throw new Error(`This app doesn't have any deploy environments.`); if (!app.deployEnvironment[env]) throw new Error(`This app doesn't have "${env}" deploy environment.`); if ((_a = app.deployEnvironment[env].volumes) === null || _a === void 0 ? void 0 : _a.find((vol) => vol.name === data.name)) throw new Error(`Volume name is existed, choose another one.`); // default volume type is "pvc" if (!data.type) data.type = "pvc"; const { buildTag, cluster: clusterSlug } = app.deployEnvironment[env]; // get cluster const { ClusterService } = await Promise.resolve().then(() => __importStar(require("../services"))); const clusterSvc = new ClusterService(this.ownership); const cluster = await clusterSvc.findOne({ slug: clusterSlug }); if (!cluster || !cluster.contextName) throw new Error(`Cluster "${clusterSlug}" not found or not verified.`); const { contextName: context } = cluster; // update db: NEW VOLUME const { AppService } = await Promise.resolve().then(() => __importStar(require("./index"))); const appSvc = new AppService(this.ownership); app = await appSvc.updateOne({ _id: app._id }, { $push: { [`deployEnvironment.${env}.volumes`]: data, }, }, { raw: true }); // add {PersistentVolumeClaim} to Kubernetes deployment const deployment = await (0, generate_deployment_v2_1.generateDeploymentV2)({ env, skipPrerelease: true, appSlug: app.slug, username: this.ownership.owner.slug, workspace: this.ownership.workspace, buildTag, }); // Apply deployment YAML await k8s_1.default.kubectlApplyContent(deployment.deploymentContent, { context }); // update db: DEPLOYMENT YAML app = await appSvc.updateOne({ _id: app._id }, { $set: { [`deployEnvironment.${env}.deploymentYaml`]: deployment.deploymentContent, // [`deployEnvironment.${env}.prereleaseDeploymentYaml`]: deployment.prereleaseDeploymentContent, }, }, { raw: true }); // result return app.deployEnvironment[env].volumes.find((vol) => vol.name === data.name); } /** * Add persistent volume to deploy environment * @param app - IApp * @param env - Deploy environment (dev, prod,...) * @param data - Persistent volume configuration */ async addPersistentVolumeBySize(app, env, data) { var _a; // validate if ((0, string_1.containsSpecialCharacters)(data.name)) throw new Error(`Volume name cannot contain any special characters.`); if (!app.deployEnvironment) throw new Error(`This app doesn't have any deploy environments.`); if (!app.deployEnvironment[env]) throw new Error(`This app doesn't have "${env}" deploy environment.`); if ((_a = app.deployEnvironment[env].volumes) === null || _a === void 0 ? void 0 : _a