UNPKG

13.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const sdk_1 = require("@cto.ai/sdk");
5const debug_1 = tslib_1.__importDefault(require("debug"));
6const fs = tslib_1.__importStar(require("fs-extra"));
7const os = tslib_1.__importStar(require("os"));
8const path = tslib_1.__importStar(require("path"));
9const uuid_1 = require("uuid");
10const env_1 = require("../constants/env");
11const CustomErrors_1 = require("../errors/CustomErrors");
12const Analytics_1 = require("./Analytics");
13const Container_1 = require("./Container");
14const Image_1 = require("./Image");
15const RegistryAuth_1 = require("./RegistryAuth");
16const utils_1 = require("../utils");
17const validate_1 = require("../utils/validate");
18const debug = debug_1.default('ops:OpService');
19class OpService {
20 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)) {
21 this.registryAuthService = registryAuthService;
22 this.imageService = imageService;
23 this.containerService = containerService;
24 this.analytics = analytics;
25 this.opsBuildLoop = async (ops, opPath, config) => {
26 const { team: { name: teamName }, user, tokens: { accessToken }, } = config;
27 for (const op of ops) {
28 if (!validate_1.isValidOpName(op.name)) {
29 throw new CustomErrors_1.InvalidInputCharacter('Op Name');
30 }
31 console.log(`🛠 ${sdk_1.ux.colors.white('Building:')} ${sdk_1.ux.colors.callOutCyan(opPath)}\n`);
32 const opImageTag = utils_1.getOpImageTag(teamName, op.name, undefined, op.isPublic);
33 await this.imageService.build(utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag), opPath, op);
34 this.analytics.track({
35 userId: user.email,
36 event: 'Ops CLI Build',
37 properties: {
38 email: user.email,
39 username: user.username,
40 name: op.name,
41 description: op.description,
42 image: `${env_1.OPS_REGISTRY_HOST}/${op.name}`,
43 },
44 }, accessToken);
45 }
46 };
47 this.updateOpFields = (inputs) => {
48 let { op, parsedArgs: { opParams }, } = inputs;
49 op.run = [op.run, ...opParams].join(' ');
50 op.runId = uuid_1.v4();
51 return Object.assign({}, inputs, { op });
52 };
53 this.getImage = async (inputs) => {
54 const { op, config, version, parsedArgs: { args: { nameOrPath }, flags: { build }, }, } = inputs;
55 try {
56 op.image = this.setOpImageUrl(op, config);
57 debug('%O', op);
58 const localImage = await this.imageService.checkLocalImage(op.image);
59 if (!localImage || build) {
60 op.isPublished
61 ? await this.pullImageFromRegistry(op, config, version)
62 : await this.imageService.build(`${op.image}`, path.resolve(process.cwd(), nameOrPath), op);
63 }
64 return inputs;
65 }
66 catch (err) {
67 if (err instanceof CustomErrors_1.UserUnauthorized) {
68 throw err;
69 }
70 debug('%O', err);
71 throw new Error('Unable to find image for this op');
72 }
73 };
74 this.pullImageFromRegistry = async (op, config, version) => {
75 const { authconfig, robotID } = await this.registryAuthService.create(config.tokens.accessToken, op.teamName, op.name, version, // TODO: change it op.version once its added but for now setting it to platform version
76 true, false);
77 // pull image
78 await this.imageService.pull(op, authconfig);
79 // delete token
80 await this.registryAuthService.delete(config.tokens.accessToken, robotID, config.team.name, op.name, version);
81 };
82 this.setOpImageUrl = (op, config) => {
83 const opIdentifier = op.isPublished ? op.id : op.name;
84 const teamName = op.teamName ? op.teamName : config.team.name;
85 const opImageTag = utils_1.getOpImageTag(teamName, opIdentifier, undefined, op.isPublic);
86 return utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag);
87 };
88 this.setEnvs = (inputs) => {
89 const { config, op } = inputs;
90 const defaultEnv = {
91 OPS_HOME: path.resolve(sdk_1.sdk.homeDir() + '/.config/@cto.ai/ops'),
92 CONFIG_DIR: `/${config.team.name}/${op.name}`,
93 STATE_DIR: `/${config.team.name}/${op.name}/${op.runId}`,
94 NODE_ENV: 'production',
95 LOGGER_PLUGINS_STDOUT_ENABLED: 'true',
96 RUN_ID: op.runId,
97 OPS_ACCESS_TOKEN: config.tokens.accessToken,
98 OPS_API_PATH: env_1.OPS_API_PATH,
99 OPS_API_HOST: env_1.OPS_API_HOST,
100 OPS_OP_ID: op.id,
101 OPS_OP_NAME: op.name,
102 OPS_TEAM_ID: config.team.id,
103 OPS_TEAM_NAME: config.team.name,
104 OPS_HOST_PLATFORM: os.platform(),
105 };
106 let opsHome = (process.env.HOME || process.env.USERPROFILE) + '/.config/@cto.ai/ops';
107 op.opsHome = opsHome === undefined ? '' : opsHome;
108 op.stateDir = `/${config.team.name}/${op.name}/${op.runId}`;
109 op.configDir = `/${config.team.name}/${op.name}`;
110 const opsYamlEnv = op.env
111 ? op.env.reduce(this.convertEnvStringsToObject, {})
112 : [];
113 op.env = Object.entries(Object.assign({}, defaultEnv, opsYamlEnv))
114 .map(this.overrideEnvWithProcessEnv(process.env))
115 .map(([key, val]) => `${key}=${val}`);
116 return Object.assign({}, inputs, { config, op });
117 };
118 this.hostSetup = (_a) => {
119 var { op } = _a, rest = tslib_1.__rest(_a, ["op"]);
120 if (!fs.existsSync(op.stateDir)) {
121 try {
122 fs.ensureDirSync(path.resolve(op.opsHome + op.stateDir));
123 }
124 catch (err) {
125 debug('%O', err);
126 throw new CustomErrors_1.CouldNotMakeDir(err, path.resolve(op.opsHome + op.stateDir));
127 }
128 }
129 return Object.assign({}, rest, { op: Object.assign({}, op, { bind: op.bind ? op.bind.map(this.replaceHomeAlias) : [] }) });
130 };
131 this.setBinds = (_a) => {
132 var { op } = _a, rest = tslib_1.__rest(_a, ["op"]);
133 return Object.assign({}, rest, { op: Object.assign({}, op, { bind: op.bind ? op.bind.map(this.replaceHomeAlias) : [] }) });
134 };
135 this.getOptions = (_a) => {
136 var { op, config } = _a, rest = tslib_1.__rest(_a, ["op", "config"]);
137 const Image = op.image;
138 const WorkingDir = op.mountCwd ? '/cwd' : '/ops';
139 const Cmd = op.run ? op.run.split(' ') : [];
140 if (op.mountCwd) {
141 const bindFrom = process.cwd();
142 const bindTo = '/cwd';
143 const cwDir = `${bindFrom}:${bindTo}`;
144 op.bind.push(cwDir);
145 }
146 if (op.mountHome) {
147 const homeDir = `${env_1.HOME}:/root:rw`;
148 op.bind.push(homeDir);
149 }
150 const stateMount = op.opsHome +
151 op.configDir +
152 ':/root/.config/@cto.ai/ops' +
153 op.configDir +
154 ':rw';
155 op.bind.push(stateMount);
156 const options = {
157 // name: `${config.team.name}-${op.name}`,
158 AttachStderr: true,
159 AttachStdin: true,
160 AttachStdout: true,
161 Cmd,
162 Env: op.env,
163 WorkingDir,
164 HostConfig: {
165 Binds: op.bind,
166 NetworkMode: op.network,
167 },
168 Image,
169 OpenStdin: true,
170 StdinOnce: false,
171 Tty: true,
172 Volumes: {},
173 VolumesFrom: [],
174 };
175 return Object.assign({}, rest, { op, options });
176 };
177 this.addPortsToOptions = async (_a) => {
178 var { op, options } = _a, rest = tslib_1.__rest(_a, ["op", "options"]);
179 /**
180 * Turns a string of ports to the syntax docker understands if it exists
181 * https://docs.docker.com/engine/api/v1.39/#operation/ContainerCreate
182 *
183 * e.g.
184 * ports:
185 * - 3000:3000
186 * - 5000:9000
187 * Will turn to
188 * PortBindings: {
189 * "3000/tcp": [
190 * {
191 * "HostPort": "3000"
192 * },
193 * "5000/tcp": [
194 * {
195 * "HostPort": "9000"
196 * }
197 * ]
198 * ExposedPorts: {
199 * "3000/tcp": {},
200 * "5000/tcp": {}
201 * }
202 */
203 const ExposedPorts = {};
204 const PortBindings = {};
205 if (op.port) {
206 const parsedPorts = op.port
207 .filter(p => !!p) // Remove null valuesT
208 .map(port => {
209 if (typeof port !== 'string')
210 throw new CustomErrors_1.YamlPortError(port);
211 const portSplits = port.split(':');
212 if (!portSplits.length || portSplits.length > 2) {
213 throw new CustomErrors_1.YamlPortError(port);
214 }
215 portSplits.forEach(p => {
216 const portNumber = parseInt(p, 10);
217 if (!portNumber)
218 throw new CustomErrors_1.YamlPortError(port);
219 });
220 return { host: portSplits[0], machine: `${portSplits[1]}/tcp` };
221 });
222 parsedPorts.forEach(parsedPorts => {
223 ExposedPorts[parsedPorts.machine] = {};
224 });
225 parsedPorts.forEach(parsedPorts => {
226 PortBindings[parsedPorts.machine] = [
227 ...(PortBindings[parsedPorts.machine] || []),
228 {
229 HostPort: parsedPorts.host,
230 },
231 ];
232 });
233 }
234 options = Object.assign({}, options, { ExposedPorts, HostConfig: Object.assign({}, options.HostConfig, { PortBindings }) });
235 return Object.assign({}, rest, { op,
236 options });
237 };
238 this.createContainer = async (inputs) => {
239 try {
240 const { op, options } = inputs;
241 const container = await this.containerService.create(op, options);
242 return Object.assign({}, inputs, { container });
243 }
244 catch (err) {
245 debug('%O', err);
246 throw new Error('Error creating Docker container');
247 }
248 };
249 this.attachToContainer = async (inputs) => {
250 const { container } = inputs;
251 if (!container)
252 throw new Error('No docker container for attachment');
253 try {
254 const options = {
255 stream: true,
256 stdin: true,
257 stdout: true,
258 stderr: true,
259 };
260 const stream = await container.attach(options);
261 this.containerService.handleStream(stream);
262 await this.containerService.start(stream);
263 return inputs;
264 }
265 catch (err) {
266 debug('%O', err);
267 throw new Error(err);
268 }
269 };
270 this.convertEnvStringsToObject = (acc, curr) => {
271 const [key, val] = curr.split('=');
272 if (!val) {
273 return Object.assign({}, acc);
274 }
275 return Object.assign({}, acc, { [key]: val });
276 };
277 this.overrideEnvWithProcessEnv = (processEnv) => ([key, val,]) => [key, processEnv[key] || val];
278 this.replaceHomeAlias = (bindPair) => {
279 const [first, ...rest] = bindPair.split(':');
280 const from = first.replace('~', env_1.HOME).replace('$HOME', env_1.HOME);
281 const to = rest.join('');
282 return `${from}:${to}`;
283 };
284 }
285 async run(op, parsedArgs, config, version) {
286 try {
287 const opServicePipeline = utils_1.asyncPipe(this.updateOpFields, this.getImage, this.setEnvs, this.hostSetup, this.setBinds, this.getOptions, this.addPortsToOptions, this.createContainer, this.attachToContainer);
288 await opServicePipeline({
289 op,
290 config,
291 parsedArgs,
292 version,
293 });
294 }
295 catch (err) {
296 debug('%O', err);
297 throw err;
298 }
299 }
300}
301exports.OpService = OpService;