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