@topgroup/diginext
Version:
A BUILD SERVER & CLI to deploy apps to any Kubernetes clusters.
404 lines (403 loc) • 19.9 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.startBuildV1 = exports.stopBuild = exports.queue = void 0;
const chalk_1 = __importDefault(require("chalk"));
const dayjs_1 = __importDefault(require("dayjs"));
const log_1 = require("diginext-utils/dist/xconsole/log");
const humanize_duration_1 = __importDefault(require("humanize-duration"));
const lodash_1 = require("lodash");
const p_queue_1 = __importDefault(require("p-queue"));
const path_1 = __importDefault(require("path"));
const const_1 = require("../../config/const");
const plugins_1 = require("../../plugins");
const mongodb_1 = require("../../plugins/mongodb");
const server_1 = require("../../server");
const get_app_environment_1 = require("../apps/get-app-environment");
const builder_1 = __importDefault(require("../builder"));
const create_build_slug_1 = require("../deploy/create-build-slug");
const generate_deployment_v2_1 = require("../deploy/generate-deployment-v2");
const git_1 = require("../git");
const k8s_1 = __importDefault(require("../k8s"));
const connect_registry_1 = require("../registry/connect-registry");
const index_1 = require("./index");
exports.queue = new p_queue_1.default({ concurrency: 1 });
/**
* Stop the build process.
*/
const stopBuild = async (projectSlug, appSlug, buildSlug) => {
const { DB } = await Promise.resolve().then(() => __importStar(require("../api/DB")));
let error;
// Validate...
if (!appSlug) {
error = `App "${appSlug}" not found.`;
(0, log_1.logError)(`[STOP_BUILD]`, error);
return { error };
}
// Stop the f*cking buildx driver...
const builderName = `${projectSlug.toLowerCase()}_${appSlug.toLowerCase()}`;
await builder_1.default.Docker.stopBuild(builderName);
// Update the status in the database
const stoppedBuild = await (0, index_1.updateBuildStatusByAppSlug)(appSlug, buildSlug, "failed");
(0, log_1.logSuccess)(`Build process of "${buildSlug}" has been stopped.`);
return stoppedBuild;
};
exports.stopBuild = stopBuild;
/**
* Start build the app with {InputOptions}
* @deprecated
*/
async function startBuildV1(options, addition = { shouldRollout: true }) {
const { DB } = await Promise.resolve().then(() => __importStar(require("../api/DB")));
// parse variables
const { shouldRollout = true } = addition;
const startTime = (0, dayjs_1.default)();
const { env = "dev", buildTag, buildImage, gitBranch, username = "Anonymous", projectSlug, slug: appSlug, namespace } = options;
const latestBuild = await DB.findOne("build", { appSlug, projectSlug, status: "success" }, { order: { createdAt: -1 } });
const app = await DB.findOne("app", { slug: appSlug }, { populate: ["owner", "workspace", "project"] });
const project = await DB.findOne("project", { slug: projectSlug });
const author = await DB.findOne("user", { _id: options.userId });
const workspace = app.workspace;
// socket & logs
const SOCKET_ROOM = (0, create_build_slug_1.createBuildSlug)({ projectSlug, appSlug, buildTag });
const logger = new plugins_1.Logger(SOCKET_ROOM);
options.SOCKET_ROOM = SOCKET_ROOM;
// Emit socket message to request the BUILD SERVER to start building...
server_1.socketIO === null || server_1.socketIO === void 0 ? void 0 : server_1.socketIO.to(SOCKET_ROOM).emit("message", { action: "start" });
// Validating...
if (!app) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `App "${appSlug}" not found.` });
return;
}
(0, log_1.log)("options :>>", JSON.stringify(options));
/**
* Specify BUILD DIRECTORY to pull source code:
* on build server, this is gonna be --> /mnt/build/{TARGET_DIRECTORY}/{REPO_BRANCH_NAME}
* /mnt/build/ -> additional disk (300GB) which mounted to this server on Digital Ocean.
*/
let buildDir = options.targetDirectory || process.cwd();
const SOURCE_CODE = `cache/${options.projectSlug}/${options.slug}/${gitBranch}`;
buildDir = path_1.default.resolve(const_1.CLI_CONFIG_DIR, SOURCE_CODE);
options.targetDirectory = buildDir;
options.buildDir = buildDir;
// detect "gitProvider":
const gitProvider = (0, plugins_1.getGitProviderFromRepoSSH)(options.repoSSH);
// create new build on build server:
const buildData = {
name: `[${options.env.toUpperCase()}] ${buildImage}`,
slug: SOCKET_ROOM,
tag: buildTag,
status: "building",
env,
createdBy: username,
projectSlug,
appSlug,
image: buildImage,
logs: logger === null || logger === void 0 ? void 0 : logger.content,
cliVersion: options.version,
app: app._id,
project: project._id,
owner: author._id,
workspace: workspace._id,
};
const newBuild = await DB.create("build", buildData);
if (!newBuild) {
console.log("buildData :>> ", buildData);
(0, index_1.sendLog)({ SOCKET_ROOM, message: "Failed to create new build on server." });
return;
}
(0, index_1.sendLog)({ SOCKET_ROOM, message: "Created new build on server!" });
// verify SSH before pulling files...
const gitAuth = await (0, git_1.verifySSH)({ gitProvider });
if (!gitAuth) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `"${buildDir}" -> Failed to verify "${gitProvider}" git SSH key.` });
await (0, index_1.updateBuildStatus)(newBuild, "failed");
return;
}
// Git SSH verified -> start pulling now...
(0, index_1.sendLog)({ SOCKET_ROOM, message: `Pulling latest source code from "${options.repoSSH}" at "${gitBranch}" branch...` });
await (0, plugins_1.pullOrCloneGitRepo)(options.repoSSH, buildDir, gitBranch, { onUpdate: (message) => (0, index_1.sendLog)({ SOCKET_ROOM, message }) });
// emit socket message to "digirelease" app:
(0, index_1.sendLog)({ SOCKET_ROOM, message: `Finished pulling latest files of "${gitBranch}"...` });
/**
* Check if Dockerfile existed
*/
let dockerFile = (0, plugins_1.resolveDockerfilePath)({ targetDirectory: buildDir, env });
if (options.isDebugging)
console.log("dockerFile :>> ", dockerFile);
if (!dockerFile) {
(0, index_1.sendLog)({
SOCKET_ROOM,
type: "error",
message: `Missing "Dockerfile" to build the application, please create your "Dockerfile" in the root directory of the source code.`,
});
return;
}
/**
* Validating app deploy environment
*/
let serverDeployEnvironment = await (0, get_app_environment_1.getDeployEvironmentByApp)(app, env);
let isPassedDeployEnvironmentValidation = true;
// validating...
if ((0, lodash_1.isEmpty)(serverDeployEnvironment)) {
(0, index_1.sendLog)({
SOCKET_ROOM,
type: "error",
message: `Deploy environment (${env.toUpperCase()}) of "${appSlug}" app is empty (probably deleted?).`,
});
isPassedDeployEnvironmentValidation = false;
}
if ((0, lodash_1.isEmpty)(serverDeployEnvironment.cluster)) {
(0, index_1.sendLog)({
SOCKET_ROOM,
type: "error",
message: `Deploy environment (${env.toUpperCase()}) of "${appSlug}" app doesn't contain "cluster" name (probably deleted?).`,
});
isPassedDeployEnvironmentValidation = false;
}
if ((0, lodash_1.isEmpty)(serverDeployEnvironment.namespace)) {
(0, index_1.sendLog)({
SOCKET_ROOM,
type: "error",
message: `Deploy environment (${env.toUpperCase()}) of "${appSlug}" app doesn't contain "namespace" name (probably deleted?).`,
});
isPassedDeployEnvironmentValidation = false;
}
if (!isPassedDeployEnvironmentValidation)
return;
/**
* Create namespace & imagePullScrets here!
* Because it will generate the name of secret to put into deployment yaml
*/
const cluster = await DB.findOne("cluster", { slug: serverDeployEnvironment.cluster }, { subpath: "/all" });
if (!cluster) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Cluster "${serverDeployEnvironment.cluster}" not found` });
return;
}
const { contextName: context } = cluster;
if (!cluster) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Cannot connect to cluster "${serverDeployEnvironment.cluster}" (context not found).` });
return;
}
const isNsExisted = await k8s_1.default.isNamespaceExisted(serverDeployEnvironment.namespace, { context });
if (!isNsExisted) {
const createNsResult = await k8s_1.default.createNamespace(serverDeployEnvironment.namespace, { context });
if (!createNsResult) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Failed to create "${serverDeployEnvironment.namespace}" namespace.` });
return;
}
}
try {
await k8s_1.default.createImagePullSecretsInNamespace(appSlug, env, serverDeployEnvironment.cluster, serverDeployEnvironment.namespace);
}
catch (e) {
(0, index_1.sendLog)({
SOCKET_ROOM,
type: "error",
message: `Can't create "imagePullSecrets" in the "${serverDeployEnvironment.namespace}" namespace of "${serverDeployEnvironment.cluster}" cluster.`,
});
return;
}
/**
* !!! IMPORTANT !!!
* Generate deployment data (YAML) & save the YAML deployment to "app.deployEnvironment[env]"
* So it can be used to create release from build
*/
let deployment;
(0, index_1.sendLog)({ SOCKET_ROOM, message: `Generating the deployment files on server...` });
try {
deployment = await (0, generate_deployment_v2_1.generateDeploymentV2)({
appSlug,
env,
username,
workspace,
buildTag: buildTag,
targetDirectory: options.targetDirectory,
});
}
catch (e) {
// save log to database
const { SystemLogService } = await Promise.resolve().then(() => __importStar(require("../../services")));
const logSvc = new SystemLogService({ owner: author, workspace });
logSvc.saveError(e, { name: "start-build" });
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: e.message });
return;
}
const { endpoint, deploymentContent } = deployment;
// sendLog({ SOCKET_ROOM, message: deploymentContent });
// if (env === "prod") sendLog({ SOCKET_ROOM, message: prereleaseDeploymentContent });
// update data to deploy environment:
// serverDeployEnvironment.prereleaseUrl = prereleaseUrl;
serverDeployEnvironment.deploymentYaml = deploymentContent;
// serverDeployEnvironment.prereleaseDeploymentYaml = prereleaseDeploymentContent;
serverDeployEnvironment.updatedAt = new Date();
serverDeployEnvironment.lastUpdatedBy = username;
// Update {user}, {project}, {environment} to database before rolling out
const updatedAppData = { environment: app.environment || {}, deployEnvironment: app.deployEnvironment || {} };
updatedAppData.lastUpdatedBy = username;
updatedAppData.deployEnvironment[env] = serverDeployEnvironment;
const [updatedApp] = await DB.update("app", { slug: appSlug }, updatedAppData);
(0, index_1.sendLog)({ SOCKET_ROOM, message: `Generated the deployment files successfully!` });
// log(`[BUILD] App's last updated by "${updatedApp.lastUpdatedBy}".`);
// build the app with Docker:
try {
(0, index_1.sendLog)({ SOCKET_ROOM, message: `Start building the Docker image...` });
// authenticate registry before building & pushing image
const registry = await DB.findOne("registry", { slug: serverDeployEnvironment.registry }, { subpath: "/all" });
await (0, connect_registry_1.connectRegistry)(registry, { userId: author._id, workspaceId: workspace._id });
const buildEngine = process.env.BUILDER === "docker" ? builder_1.default.Docker : builder_1.default.Podman;
await buildEngine.build(buildImage, {
platforms: ["linux/amd64"],
builder: `${projectSlug.toLowerCase()}_${appSlug.toLowerCase()}`,
cacheFroms: latestBuild ? [{ type: "registry", value: latestBuild.image }] : [],
dockerFile: dockerFile,
buildDirectory: buildDir,
shouldPush: true,
onBuilding: (message) => (0, index_1.sendLog)({ SOCKET_ROOM, message }),
});
// update build status as "success"
await (0, index_1.updateBuildStatus)(newBuild, "success", { env });
(0, index_1.sendLog)({
SOCKET_ROOM,
message: `✓ Built a Docker image & pushed to container registry (${serverDeployEnvironment.registry}) successfully!`,
});
}
catch (e) {
await (0, index_1.updateBuildStatus)(newBuild, "failed");
(0, index_1.sendLog)({ SOCKET_ROOM, message: e.message, type: "error" });
return;
}
/**
* ! If this is a Next.js project, upload ".next/static" to CDN:
*/
// TODO: enable upload cdn while building source code:
// const nextStaticDir = path.resolve(options.targetDirectory, ".next/static");
// if (existsSync(nextStaticDir) && diginext.environment[env].cdn) {
// options.secondAction = "push";
// options.thirdAction = nextStaticDir;
// await execCDN(options);
// }
// Insert this build record to server:
// let prereleaseDeploymentData = fetchDeploymentFromContent(prereleaseDeploymentContent);
let releaseId, newRelease;
try {
newRelease = await (0, index_1.createReleaseFromBuild)(newBuild, env, { author });
releaseId = mongodb_1.MongoDB.toString(newRelease._id);
// log("Created new Release successfully:", newRelease);
(0, index_1.sendLog)({ SOCKET_ROOM, message: `✓ Created new release "${SOCKET_ROOM}" (ID: ${releaseId}) on BUILD SERVER successfully.` });
}
catch (e) {
(0, index_1.sendLog)({ SOCKET_ROOM, message: `${e.message}`, type: "error" });
return;
}
if (!shouldRollout) {
const buildDuration = (0, dayjs_1.default)().diff(startTime, "millisecond");
(0, index_1.sendLog)({
SOCKET_ROOM,
message: chalk_1.default.green(`🎉 FINISHED BUILDING IMAGE AFTER ${(0, humanize_duration_1.default)(buildDuration)} 🎉`),
type: "success",
});
return { build: newBuild, release: newRelease };
}
/**
* ! [WARNING]
* ! If "--fresh" flag was specified, the deployment's namespace will be deleted & redeploy from scratch!
*/
console.log("options.shouldUseFreshDeploy :>> ", options.shouldUseFreshDeploy);
if (options.shouldUseFreshDeploy) {
(0, index_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 "${options.namespace}" namespace (APP: ${appSlug} / PROJECT: ${projectSlug}) shortly...`,
});
const wipedNamespaceRes = await k8s_1.default.deleteNamespaceByCluster(options.namespace, serverDeployEnvironment.cluster);
if ((0, lodash_1.isEmpty)(wipedNamespaceRes)) {
(0, index_1.sendLog)({
SOCKET_ROOM,
type: "error",
message: `Unable to delete "${options.namespace}" namespace of "${serverDeployEnvironment.cluster}" cluster (APP: ${appSlug} / PROJECT: ${projectSlug}).`,
});
return;
}
(0, index_1.sendLog)({
SOCKET_ROOM,
message: `Successfully deleted "${options.namespace}" namespace of "${serverDeployEnvironment.cluster}" cluster (APP: ${appSlug} / PROJECT: ${projectSlug}).`,
});
}
if (releaseId) {
(0, index_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...`,
});
const onRolloutUpdate = (msg) => (0, index_1.sendLog)({ SOCKET_ROOM, message: msg });
try {
const result = env === "prod"
? await k8s_1.default.previewPrerelease(releaseId, { onUpdate: onRolloutUpdate })
: await k8s_1.default.rollout(releaseId, { onUpdate: onRolloutUpdate });
if (result.error) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Failed to roll out the release :>> ${result.error}.` });
return;
}
newRelease = result.data;
}
catch (e) {
(0, index_1.sendLog)({ SOCKET_ROOM, type: "error", message: `Failed to roll out the release :>> ${e.message}:` });
return;
}
}
// Print success:
const deployDuration = (0, dayjs_1.default)().diff(startTime, "millisecond");
(0, index_1.sendLog)({ SOCKET_ROOM, message: chalk_1.default.green(`✅ FINISHED BUILDING AFTER ${(0, humanize_duration_1.default)(deployDuration)}`), type: "success" });
// if (env == "prod") {
// const { buildServerUrl } = getCliConfig();
// const rollOutUrl = `${buildServerUrl}/project/?lv1=release&project=${projectSlug}&app=${appSlug}&env=prod`;
// sendLog({ SOCKET_ROOM, message: chalk.bold(chalk.yellow(`✓ Preview at: ${prereleaseDeploymentData.endpoint}`)), type: "success" });
// sendLog({
// SOCKET_ROOM,
// message: chalk.bold(chalk.yellow(`✓ Review & publish at: ${rollOutUrl}`)),
// type: "success",
// });
// sendLog({
// SOCKET_ROOM,
// message: chalk.bold(chalk.yellow(`✓ Roll out with CLI command:`), `$ dx rollout ${releaseId}`),
// type: "success",
// });
// } else {
// sendLog({ SOCKET_ROOM, message: chalk.bold(chalk.yellow(`✓ Preview at: ${endpoint}`)), type: "success" });
// }
(0, index_1.sendLog)({ SOCKET_ROOM, message: chalk_1.default.bold(chalk_1.default.yellow(`✓ Preview at: ${endpoint}`)), type: "success" });
// i don't know, just for sure...
await (0, plugins_1.wait)(2000);
// disconnect CLI client:
server_1.socketIO === null || server_1.socketIO === void 0 ? void 0 : server_1.socketIO.to(SOCKET_ROOM).emit("message", { action: "end" });
// logSuccess(msg);
return { build: newBuild, release: newRelease };
}
exports.startBuildV1 = startBuildV1;