UNPKG

@topgroup/diginext

Version:

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

404 lines (403 loc) 19.9 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.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;