@cto.ai/ops
Version:
š» CTO.ai - The CLI built for Teams š
193 lines (192 loc) ⢠9.02 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageService = void 0;
const tslib_1 = require("tslib");
const JSONStream_1 = tslib_1.__importDefault(require("JSONStream"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("path"));
const through2_1 = tslib_1.__importDefault(require("through2"));
const cli_sdk_1 = require("@cto.ai/cli-sdk");
const opConfig_1 = require("../constants/opConfig");
const CustomErrors_1 = require("./../errors/CustomErrors");
const Error_1 = require("./../services/Error");
const get_docker_1 = tslib_1.__importDefault(require("./../utils/get-docker"));
const debug = (0, debug_1.default)('ops:ImageService');
class ImageService {
constructor(error = new Error_1.ErrorService()) {
this.error = error;
this.log = console.log;
this.checkLocalImage = async (opImageUrl) => {
const docker = await (0, get_docker_1.default)(console, 'ImageService');
const list = await docker.listImages();
return list
.map(this.imageFilterPredicate(opImageUrl))
.find((repoTag) => !!repoTag);
};
this.imageFilterPredicate = (repo) => ({ RepoTags }) => {
if (!RepoTags) {
return;
}
return RepoTags.find((repoTag) => repoTag.includes(repo));
};
this.pull = async (op, authconfig) => {
const docker = await (0, get_docker_1.default)(console, 'ImageServicePull');
const stream = await docker
.pull(op.image || '', { authconfig })
.catch(err => {
throw new CustomErrors_1.ImagePullError(err);
});
if (!stream) {
throw new Error('No stream');
}
this.log(`š Pulling ${cli_sdk_1.ux.colors.dim(op.name)} from registry...\n`);
const parser = this.setParser(op, this.getProgressBarText);
await new Promise(this.updateStatusBar(stream, parser));
cli_sdk_1.ux.spinner.stop(cli_sdk_1.ux.colors.green('Done!'));
const msg = `${cli_sdk_1.ux.colors.italic.bold(`${op.name}:${op.id}`)}`;
this.log(`\nš Saved ${msg} locally! \n`);
};
this.setParser = (op, getFn) => {
const bar = cli_sdk_1.ux.progress.init({
format: cli_sdk_1.ux.colors.callOutCyan('{bar} {percentage}% | Status: {speed} '),
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
});
bar.start(100, 0, { speed: 'š Starting...' });
const layers = {};
const parser = through2_1.default.obj((chunk, _enc, callback) => {
const { id, status, progressDetail: p } = chunk;
const progress = p && p.current ? (p.current / p.total) * 100 : 0;
const { speed } = getFn(status, op);
if (id) {
layers[id] = id;
}
if (speed) {
bar.update(progress, { speed });
}
callback();
});
const _pipe = parser.pipe;
parser.pipe = dest => _pipe(dest);
return { parser, bar };
};
this.getProgressBarText = (status, { name }) => {
const mapping = {
[`Pulling from ${name}`]: `ā
Pulling from ${name}...`,
'Already exists': 'ā
Already exists!',
Waiting: 'ā± Waiting...',
Downloading: 'š Downloading...',
'Download complete': 'š Downloaded!',
Extracting: 'š¦ Unpacking...',
'Pulling fs layer': 'š Pulling layers...',
'Pull complete': 'š Pull Complete!',
};
return { speed: mapping[status] };
};
this.updateStatusBar = (stream, { parser, bar }) => async (resolve, reject) => {
const allData = [];
const size = 100;
stream
.pipe(JSONStream_1.default.parse(undefined))
.pipe(parser)
.on('data', data => allData.push(data))
.on('end', async (err) => {
for (let i = 0; i < size; i++) {
bar.update(100 - size / i);
await cli_sdk_1.ux.wait(5);
}
bar.update(100);
bar.stop();
debug('%O', err);
return err ? reject(err) : resolve(allData);
});
};
this.checkIfDockerfileExists = (opPath) => {
const pathToDockerfile = path.join(path.resolve(opPath), 'Dockerfile');
return fs.existsSync(pathToDockerfile);
};
this.build = async (tag, opPath, op, options) => {
const ogPath = opPath;
if (op.type === opConfig_1.JOB_TYPE) {
opPath = ogPath + '/.ops/jobs/' + op.name;
}
try {
const dockerfileExists = this.checkIfDockerfileExists(opPath);
if (!dockerfileExists) {
throw new Error(`Unable to build an image for this workflow. If you are inside your workflow directory, please run ${cli_sdk_1.ux.colors.green('$')} ${cli_sdk_1.ux.colors.italic.dim('ops run .')} or ${cli_sdk_1.ux.colors.green('$')} ${cli_sdk_1.ux.colors.italic.dim('ops build .')} instead.\n`);
}
const all = [];
const errors = [];
const log = this.log;
const parser = through2_1.default.obj(function (chunk, _enc, cb) {
if (chunk.stream && chunk.stream !== '\n') {
this.push(chunk.stream.replace('\n', ''));
log(chunk.stream.replace('\n', ''));
all.push(chunk);
}
else if (chunk.errorDetail) {
debug(chunk.errorDetail);
errors.push(chunk.errorDetail.message);
}
cb();
});
const _pipe = parser.pipe;
parser.pipe = function (dest) {
return _pipe(dest);
};
const docker = await (0, get_docker_1.default)(console, 'build');
// TODO: What error handling should we do if this is falsy
if (!docker) {
return;
}
const defaultOptions = {
t: tag,
nocache: false,
pull: true,
};
const buildOptions = Object.assign(Object.assign({}, defaultOptions), options);
debug('build options ', buildOptions);
const stream = await docker
.buildImage({ context: opPath, src: op.src || fs.readdirSync(opPath) }, buildOptions)
.catch(err => {
debug('%O', err);
throw new CustomErrors_1.DockerBuildImageError(err);
});
// TODO: What error handling should we do if this is false
if (!stream) {
return;
}
await new Promise(function (resolve, reject) {
stream
.pipe(JSONStream_1.default.parse(undefined))
.pipe(parser)
.on('data', (d, data) => {
all.push(d);
})
.on('end', function () {
if (errors.length) {
return reject(new CustomErrors_1.DockerBuildImageError(errors[0]));
}
let messagePath = ogPath;
const parentFolder = ogPath.substring(0, ogPath.lastIndexOf('/'));
if (process.env.PWD === ogPath) {
messagePath = '.';
}
else if (process.env.PWD === parentFolder) {
messagePath = ogPath.substring(ogPath.lastIndexOf('/') + 1);
}
log(`\nš» Run ${cli_sdk_1.ux.colors.green('$')} ${cli_sdk_1.ux.colors.italic.dim('ops run ' + messagePath)} to test your workflow.`);
log(`š¦ Run ${cli_sdk_1.ux.colors.green('$')} ${cli_sdk_1.ux.colors.italic.dim('ops publish ' + messagePath)} to share your workflow. \n`);
resolve(undefined);
});
});
}
catch (err) {
debug('%O', err);
this.error.handleError({ err });
}
};
}
}
exports.ImageService = ImageService;