@cto.ai/ops
Version:
💻 CTO.ai Ops - The CLI built for Teams 🚀
312 lines (311 loc) • 14 kB
JavaScript
;
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;