1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const sdk_1 = require("@cto.ai/sdk");
|
5 | const debug_1 = tslib_1.__importDefault(require("debug"));
|
6 | const fs = tslib_1.__importStar(require("fs-extra"));
|
7 | const os = tslib_1.__importStar(require("os"));
|
8 | const path = tslib_1.__importStar(require("path"));
|
9 | const uuid_1 = require("uuid");
|
10 | const env_1 = require("../constants/env");
|
11 | const CustomErrors_1 = require("../errors/CustomErrors");
|
12 | const Analytics_1 = require("./Analytics");
|
13 | const Container_1 = require("./Container");
|
14 | const Image_1 = require("./Image");
|
15 | const RegistryAuth_1 = require("./RegistryAuth");
|
16 | const utils_1 = require("../utils");
|
17 | const validate_1 = require("../utils/validate");
|
18 | const debug = debug_1.default('ops:OpService');
|
19 | class 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,
|
76 | true, false);
|
77 |
|
78 | await this.imageService.pull(op, authconfig);
|
79 |
|
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 |
|
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 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 | const ExposedPorts = {};
|
204 | const PortBindings = {};
|
205 | if (op.port) {
|
206 | const parsedPorts = op.port
|
207 | .filter(p => !!p)
|
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 | }
|
301 | exports.OpService = OpService;
|