@topgroup/diginext
Version:
A BUILD SERVER & CLI to deploy apps to any Kubernetes clusters.
940 lines • 55.4 kB
JavaScript
"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