UNPKG

@cto.ai/ops

Version:

💻 CTO.ai Ops - The CLI built for Teams 🚀

312 lines (311 loc) • 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const sdk_1 = require("@cto.ai/sdk"); const debug_1 = tslib_1.__importDefault(require("debug")); const fs = tslib_1.__importStar(require("fs-extra")); const os = tslib_1.__importStar(require("os")); const path = tslib_1.__importStar(require("path")); const uuid_1 = require("uuid"); const env_1 = require("../constants/env"); const opConfig_1 = require("../constants/opConfig"); const CustomErrors_1 = require("../errors/CustomErrors"); const ErrorTemplate_1 = require("../errors/ErrorTemplate"); const Analytics_1 = require("./Analytics"); const Container_1 = require("./Container"); const Image_1 = require("./Image"); const RegistryAuth_1 = require("./RegistryAuth"); const utils_1 = require("../utils"); const validate_1 = require("../utils/validate"); const debug = debug_1.default('ops:OpService'); class OpService { constructor(registryAuthService = new RegistryAuth_1.RegistryAuthService(), imageService = new Image_1.ImageService(), containerService = new Container_1.ContainerService(), analytics = new Analytics_1.AnalyticsService(env_1.OPS_SEGMENT_KEY)) { this.registryAuthService = registryAuthService; this.imageService = imageService; this.containerService = containerService; this.analytics = analytics; this.opsBuildLoop = async (ops, opPath, config) => { const { team: { name: teamName }, user, tokens: { accessToken }, } = config; for (const op of ops) { if (!('run' in op)) continue; if (!validate_1.isValidOpName(op.name)) { throw new CustomErrors_1.InvalidInputCharacter('Op Name'); } if (!validate_1.isValidOpVersion(op)) { throw new CustomErrors_1.InvalidOpVersionFormat(); } console.log(`🛠 ${sdk_1.ux.colors.white('Building:')} ${sdk_1.ux.colors.callOutCyan(op.name + ':' + op.version)}\n`); const opImageTag = utils_1.getOpImageTag(teamName, op.name, op.version, op.isPublic); await this.imageService.build(utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag), opPath, op); this.analytics.track({ userId: user.email, cliEvent: 'Ops CLI Build', event: 'Ops CLI Build', properties: { name: op.name, team: teamName, namespace: `@${teamName}/${op.name}`, runtime: 'CLI', email: user.email, username: user.username, description: op.description, image: `${env_1.OPS_REGISTRY_HOST}/${op.name}:${op.version}`, }, }, accessToken); } }; this.updateOpFields = (inputs) => { let { op, parsedArgs: { opParams }, } = inputs; if (op.sdk === opConfig_1.SDK2) { op.run = `${opConfig_1.SDK2_DAEMON_ENTRYPOINT} ${op.run}`; } op.run = [op.run, ...opParams].join(' '); op.runId = uuid_1.v4(); return Object.assign(Object.assign({}, inputs), { op }); }; this.getImage = async (inputs) => { const { op, config, version, parsedArgs: { args: { nameOrPath }, flags: { build }, }, } = inputs; try { op.image = this.setOpImageUrl(op, config); const localImage = await this.imageService.checkLocalImage(op.image); if (!localImage || build) { op.isPublished ? await this.pullImageFromRegistry(op, config, version) : await this.imageService.build(`${op.image}`, path.resolve(process.cwd(), nameOrPath), op); } return inputs; } catch (err) { if (err instanceof ErrorTemplate_1.ErrorTemplate) { throw err; } debug('%O', err); throw new Error('Unable to find image for this op'); } }; this.pullImageFromRegistry = async (op, config, version) => { const { authconfig } = await this.registryAuthService.create(config.tokens.accessToken, op.teamName, op.name, version, true, false); // pull image await this.imageService.pull(op, authconfig); }; this.setOpImageUrl = (op, config) => { const opIdentifier = op.isPublished ? op.id : op.name; const teamName = op.teamName ? op.teamName : config.team.name; const opImageTag = utils_1.getOpImageTag(teamName, opIdentifier, op.version, op.isPublic); return utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag); }; this.setEnvs = (inputs) => { const { config, op } = inputs; const defaultEnv = { OPS_HOME: path.resolve(sdk_1.sdk.homeDir() + '/.config/@cto.ai/ops'), CONFIG_DIR: `/${config.team.name}/${op.name}`, STATE_DIR: `/${config.team.name}/${op.name}/${op.runId}`, NODE_ENV: 'production', LOGGER_PLUGINS_STDOUT_ENABLED: 'true', RUN_ID: op.runId, OPS_ACCESS_TOKEN: config.tokens.accessToken, OPS_API_PATH: env_1.OPS_API_PATH, OPS_API_HOST: env_1.OPS_API_HOST, OPS_OP_ID: op.id, OPS_OP_NAME: op.name, OPS_TEAM_ID: config.team.id, OPS_TEAM_NAME: config.team.name, OPS_HOST_PLATFORM: os.platform(), }; let opsHome = (process.env.HOME || process.env.USERPROFILE) + '/.config/@cto.ai/ops'; op.opsHome = opsHome === undefined ? '' : opsHome; op.stateDir = `/${config.team.name}/${op.name}/${op.runId}`; op.configDir = `/${config.team.name}/${op.name}`; const opsYamlEnv = op.env ? op.env.reduce(this.convertEnvStringsToObject, {}) : {}; op.env = Object.entries(Object.assign(Object.assign({}, defaultEnv), opsYamlEnv)) .map(this.overrideEnvWithProcessEnv(process.env)) .map(([key, val]) => `${key}=${val}`); return Object.assign(Object.assign({}, inputs), { config, op }); }; this.hostSetup = (_a) => { var { op } = _a, rest = tslib_1.__rest(_a, ["op"]); if (!fs.existsSync(op.stateDir)) { try { fs.ensureDirSync(path.resolve(op.opsHome + op.stateDir)); } catch (err) { debug('%O', err); throw new CustomErrors_1.CouldNotMakeDir(err, path.resolve(op.opsHome + op.stateDir)); } } return Object.assign(Object.assign({}, rest), { op: Object.assign(Object.assign({}, op), { bind: op.bind ? op.bind.map(this.replaceHomeAlias) : [] }) }); }; this.setBinds = (_a) => { var { op } = _a, rest = tslib_1.__rest(_a, ["op"]); return Object.assign(Object.assign({}, rest), { op: Object.assign(Object.assign({}, op), { bind: op.bind ? op.bind.map(this.replaceHomeAlias) : [] }) }); }; this.getOptions = (_a) => { var { op, config } = _a, rest = tslib_1.__rest(_a, ["op", "config"]); const Image = op.image; const WorkingDir = op.mountCwd ? '/cwd' : '/ops'; const Cmd = op.run ? op.run.split(' ') : []; if (op.mountCwd) { const bindFrom = process.cwd(); const bindTo = '/cwd'; const cwDir = `${bindFrom}:${bindTo}`; op.bind.push(cwDir); } if (op.mountHome) { const homeDir = `${env_1.HOME}:/root:rw`; op.bind.push(homeDir); } const stateMount = op.opsHome + op.configDir + ':/root/.config/@cto.ai/ops' + op.configDir + ':rw'; op.bind.push(stateMount); const options = { // name: `${config.team.name}-${op.name}`, AttachStderr: true, AttachStdin: true, AttachStdout: true, Cmd, Env: op.env, WorkingDir, HostConfig: { Binds: op.bind, NetworkMode: op.network, }, Image, OpenStdin: true, StdinOnce: false, Tty: true, Volumes: {}, VolumesFrom: [], }; return Object.assign(Object.assign({}, rest), { op, options }); }; this.addPortsToOptions = async (_a) => { var { op, options } = _a, rest = tslib_1.__rest(_a, ["op", "options"]); /** * Turns a string of ports to the syntax docker understands if it exists * https://docs.docker.com/engine/api/v1.39/#operation/ContainerCreate * * e.g. * ports: * - 3000:3000 * - 5000:9000 * Will turn to * PortBindings: { * "3000/tcp": [ * { * "HostPort": "3000" * }, * "5000/tcp": [ * { * "HostPort": "9000" * } * ] * ExposedPorts: { * "3000/tcp": {}, * "5000/tcp": {} * } */ const ExposedPorts = {}; const PortBindings = {}; if (op.port) { const parsedPorts = op.port .filter(p => !!p) // Remove null valuesT .map(port => { if (typeof port !== 'string') throw new CustomErrors_1.YamlPortError(port); const portSplits = port.split(':'); if (!portSplits.length || portSplits.length > 2) { throw new CustomErrors_1.YamlPortError(port); } portSplits.forEach(p => { const portNumber = parseInt(p, 10); if (!portNumber) throw new CustomErrors_1.YamlPortError(port); }); return { host: portSplits[0], machine: `${portSplits[1]}/tcp` }; }); parsedPorts.forEach(parsedPorts => { ExposedPorts[parsedPorts.machine] = {}; }); parsedPorts.forEach(parsedPorts => { PortBindings[parsedPorts.machine] = [ ...(PortBindings[parsedPorts.machine] || []), { HostPort: parsedPorts.host, }, ]; }); } options = Object.assign(Object.assign({}, options), { ExposedPorts, HostConfig: Object.assign(Object.assign({}, options.HostConfig), { PortBindings }) }); return Object.assign(Object.assign({}, rest), { op, options }); }; this.createContainer = async (inputs) => { try { const { op, options } = inputs; const container = await this.containerService.create(op, options); return Object.assign(Object.assign({}, inputs), { container }); } catch (err) { debug('%O', err); throw new Error('Error creating Docker container'); } }; this.attachToContainer = async (inputs) => { const { container } = inputs; if (!container) throw new Error('No docker container for attachment'); try { const options = { stream: true, stdin: true, stdout: true, stderr: true, }; const stream = await container.attach(options); this.containerService.handleStream(stream); await this.containerService.start(stream); return inputs; } catch (err) { debug('%O', err); throw new Error(err); } }; this.convertEnvStringsToObject = (acc, curr) => { const [key, val] = curr.split('='); if (!val) { return Object.assign({}, acc); } return Object.assign(Object.assign({}, acc), { [key]: val }); }; this.overrideEnvWithProcessEnv = (processEnv) => ([key, val,]) => [key, processEnv[key] || val]; this.replaceHomeAlias = (bindPair) => { const [first, ...rest] = bindPair.split(':'); const from = first.replace('~', env_1.HOME).replace('$HOME', env_1.HOME); const to = rest.join(''); return `${from}:${to}`; }; } async run(op, parsedArgs, config, version) { try { const opServicePipeline = utils_1.asyncPipe(this.updateOpFields, this.getImage, this.setEnvs, this.hostSetup, this.setBinds, this.getOptions, this.addPortsToOptions, this.createContainer, this.attachToContainer); await opServicePipeline({ op, config, parsedArgs, version, }); } catch (err) { debug('%O', err); throw err; } } } exports.OpService = OpService;