@cto.ai/ops
Version:
💻 CTO.ai - The CLI built for Teams 🚀
262 lines (261 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseYaml = void 0;
const tslib_1 = require("tslib");
const cli_sdk_1 = require("@cto.ai/cli-sdk");
const yaml = tslib_1.__importStar(require("yaml"));
const CustomErrors_1 = require("./../errors/CustomErrors");
const opConfig_1 = require("../constants/opConfig");
const validate_1 = require("./validate");
const _1 = require(".");
const { white, callOutCyan } = cli_sdk_1.ux.colors;
const checkCommandNecessryFields = (commandContents) => {
if (!commandContents.run || typeof commandContents.run !== 'string') {
throw new CustomErrors_1.IncompleteOpsYml('The run command must be included as a string');
}
};
const checkWorkflowNecessryFields = (workflowContents) => {
if (!workflowContents.steps || !workflowContents.steps.length) {
throw new CustomErrors_1.IncompleteOpsYml('The run command must be included as a string');
}
workflowContents.steps.forEach(step => {
if (!step || typeof step !== 'string') {
throw new CustomErrors_1.IncompleteOpsYml('Each step should be a non-empty string');
}
});
};
const checkJobProperites = (pipeline, job) => {
if (job && !job.name) {
throw new CustomErrors_1.IncompleteOpsYml('Each pipeline job name should be a non-empty string');
}
if (job && !job.description) {
throw new CustomErrors_1.IncompleteOpsYml('Each pipeline job description should be a non-empty string');
}
if (job && !job.steps) {
job.linked = true;
job.steps = [];
}
if (job && job.package !== undefined && !job.linked) {
throw new CustomErrors_1.IncompleteOpsYml(`The pipeline '${pipeline}', job '${job.name}' must include a valid \'packages\' property instead of \'package\'`);
}
};
const formatPipeline = (pipelines) => {
pipelines.forEach(pipeline => {
const nameAndVersion = _splitOpNameAndVersion(pipeline);
pipeline.name = nameAndVersion[0];
if (!nameAndVersion[1] || nameAndVersion[1] === '') {
pipeline.version = '0.1.0'; //default version
}
else {
pipeline.version = nameAndVersion[1];
}
pipeline.remote = pipeline.remote === undefined ? true : pipeline.remote;
pipeline.isPublic = pipeline.public !== undefined ? pipeline.public : false;
delete pipeline.public;
pipeline.type = opConfig_1.PIPELINE_TYPE;
});
};
const checkRequiredJobFields = (pipelines) => {
pipelines.forEach(pipeline => {
if (!pipeline.version) {
throw new CustomErrors_1.IncompleteOpsYml('Each pipeline a valid version string');
}
if (!pipeline.jobs || pipeline.jobs.length < 1) {
throw new CustomErrors_1.IncompleteOpsYml('Each pipeline requires defining jobs with at least one job');
}
if (pipeline.jobs !== undefined && Array.isArray(pipeline.jobs))
pipeline.jobs.some(x => checkJobProperites(pipeline.name, x));
if (pipeline.events !== undefined && Array.isArray(pipeline.events))
pipeline.events.some(x => validateEvents(pipeline.name, x));
});
};
const validateEvents = (pipeline, e) => {
if (e &&
e.includes(_1.PULL_REQUEST_LABELED) &&
!_1.PR_LABELED_EQUAL_REGEX.test(e)) {
throw new CustomErrors_1.IncompleteOpsYml(`The event (${e}) doesn't match the expected formats (pull_request.labeled.*.name == \'[LABEL_NAME]\' or pull_request.labeled.name == \'[LABEL_NAME]\')`);
}
};
const checkOpNecessaryFields = (opContents) => {
if (!opContents.name || typeof opContents.name !== 'string') {
throw new CustomErrors_1.IncompleteOpsYml('Workflow name must be a non-empty string');
}
if (!opContents.description || typeof opContents.description !== 'string') {
throw new CustomErrors_1.IncompleteOpsYml('Workflow description must be a non-empty string');
}
return true;
};
const checkEnvVarFormat = (yamlFile) => {
const ops = [].concat(yamlFile.ops, yamlFile.pipelines, yamlFile.services, yamlFile.workflows);
ops.forEach(v => {
if (v && v.env) {
const envs = [].concat(v.env.secrets, v.env.configs, v.env.static);
if (v.env.static !== undefined && Array.isArray(v.env.static)) {
v.env.static.forEach(e => {
if (e && e.includes('=')) {
const [key, value] = e.split('=');
if (_1.ENVS_WRONG_INDENTATION_WITH_VALUE.test(e)) {
throw new CustomErrors_1.IncompleteOpsYml(`Wrong indentation detected in ${key} ${value}`);
}
}
else if (e) {
throw new CustomErrors_1.IncompleteOpsYml(`Env static variable ${e} needs a value`);
}
});
}
envs.forEach(e => {
if (e && e.includes('=')) {
const [key, value] = e.split('=');
if (key === '' || value === '') {
throw new CustomErrors_1.IncompleteOpsYml(`Env variable ${e} is missing a key or value between it's =`);
}
if (!_1.KEY_REGEX.test(key)) {
throw new CustomErrors_1.IncompleteOpsYml(`Env key ${cli_sdk_1.ux.colors.callOutCyan(key)} is malformed. \n Env keys cannot begin with a number and may only contain letters, numbers, underscores, hyphens, and periods`);
}
}
if (e && _1.ENVS_WRONG_INDENTATION_WITHOUT_VALUE.test(e)) {
throw new CustomErrors_1.IncompleteOpsYml(`Wrong indentation detected in ${e}`);
}
});
}
});
};
const _splitOpNameAndVersion = (op) => {
// Pass through to the (later) validation code
if (!op.name || typeof op.name != 'string') {
return ['', ''];
}
const splits = op.name.split(':');
return [splits[0], splits[1]];
};
const formatAndCheckService = (service) => {
let [opName, opVersion] = _splitOpNameAndVersion(service);
if (!opVersion) {
throw new CustomErrors_1.IncompleteOpsYml(`Service ${opName} must have a version`);
}
if (!service.run || typeof service.run !== 'string') {
throw new CustomErrors_1.IncompleteOpsYml(`The run command must be included as a string for service ${opName}`);
}
if (service.domain) {
if (!(0, validate_1.isValidServiceDomain)(service.domain)) {
throw new CustomErrors_1.IncompleteOpsYml(`The domain field (${service.domain}) doesn’t match expected format, please provide just the domain without the scheme protocol`);
}
}
service.isPublic = service.public;
delete service.public;
return Object.assign(Object.assign({}, service), { name: opName, version: opVersion, type: opConfig_1.SERVICE_TYPE });
};
const parseYaml = (manifest) => {
const parse = parseAndValidateYMLSyntax(manifest);
// This will always return the default values of these variables, even if manifest is empty
const { version, ops = [], workflows = [], pipelines = [], commands = [], services = [], } = manifest && parse;
if (ops.length > 0) {
console.log(white(`It looks like your ops.yml is a little out of date.\nYou should replace the ${callOutCyan('ops')} field with ${callOutCyan('commands')}.\nLearn more here ${cli_sdk_1.ux.url('https://cto.ai/docs/developing-ops/configuring-ops', 'https://cto.ai/docs/developing-ops/configuring-ops')}`));
}
let yamlContents = {
version,
ops: [],
pipelines: [],
workflows: [],
services: services.map(formatAndCheckService),
};
yamlContents.ops = [...ops, ...commands].map(op => {
const newCommand = formatRequiredFields(op, opConfig_1.COMMAND_TYPE);
checkOpNecessaryFields(newCommand);
checkCommandNecessryFields(newCommand);
return newCommand;
});
yamlContents.workflows = workflows.map(wf => {
const newWf = formatRequiredFields(wf, opConfig_1.WORKFLOW_TYPE);
checkOpNecessaryFields(newWf);
checkWorkflowNecessryFields(newWf);
return newWf;
});
if (pipelines.length > 0) {
if (version === undefined) {
throw new CustomErrors_1.IncompleteOpsYml('The ops.yml file must have a version defined');
}
formatPipeline(pipelines);
checkRequiredJobFields(pipelines);
yamlContents.pipelines = pipelines;
}
checkEnvVarFormat(yamlContents);
const manifestLines = manifest.split(_1.LINE_BREAK);
let invalidListElements = manifestLines.filter((l, i) => {
const previousLine = manifestLines[i - 1];
if (previousLine !== undefined &&
(previousLine.includes('events:') ||
previousLine.includes('static:') ||
previousLine.includes('secrets:') ||
previousLine.includes('configs:') ||
previousLine.includes('packages:') ||
previousLine.includes('steps:'))) {
return _1.INVALID_YAML_LIST_ELEMENT.test(l);
}
});
if (invalidListElements.length > 0) {
throw new CustomErrors_1.IncompleteOpsYml(`It has invalid list elements, these are not supported:\n${cli_sdk_1.ux.colors.errorRed(invalidListElements.map(x => `${x.trim()}`).join('\n'))}`);
}
let nullValues = getNullValues([
...yamlContents.ops,
...yamlContents.pipelines,
...yamlContents.workflows,
...yamlContents.services,
]);
if (nullValues.length > 0) {
throw new CustomErrors_1.IncompleteOpsYml(`It has null values, these are not supported:\n${cli_sdk_1.ux.colors.errorRed(nullValues.join('\n').replace(/ +/g, ' '))}`);
}
return yamlContents;
};
exports.parseYaml = parseYaml;
const defaultVersion = `0.1.0`;
const defaultVersionLog = `\nℹ️ It looks like your ops.yml is a little out of date. It does not have a version, we are setting the default version to ${cli_sdk_1.ux.colors.callOutCyan(defaultVersion)}. Learn more ${cli_sdk_1.ux.url('here', 'https://cto.ai/docs/developing-ops/configuring-ops')}.\n`;
const formatRequiredFields = (opOrWorkflow, type) => {
const newOp = Object.assign({}, opOrWorkflow);
newOp.isPublic = newOp.public;
delete newOp.public;
let [opName, opVersion] = _splitOpNameAndVersion(opOrWorkflow);
if (!opVersion) {
opVersion = defaultVersion;
console.log(defaultVersionLog);
}
newOp.remote = newOp.remote === undefined ? true : newOp.remote;
newOp.name = opName;
newOp.version = opVersion;
newOp.type = type;
return newOp;
};
const getNullValues = (object, nullElements = [], pathKey = '', parentJob = '') => {
Object.keys(object).some(function (k) {
let key = isNaN(parseInt(k)) ? `${k}` : `[${k}]`;
if (object[k] === null) {
nullElements.push(`${pathKey}${key}`);
}
if (object[k] &&
typeof object[k] === 'object' &&
object[k].name !== undefined) {
parentJob = `${object[k].name} `;
}
if (object[k] && typeof object[k] === 'object') {
if (parentJob !== undefined) {
key = `${key} ${parentJob}`;
// @ts-ignore
parentJob = undefined;
}
let path = `${pathKey} ${key} `;
if (pathKey == '') {
path = object[k].type + path;
}
getNullValues(object[k], nullElements, path, parentJob);
}
});
return nullElements;
};
const parseAndValidateYMLSyntax = (manifest) => {
try {
return yaml.parse(manifest, { prettyErrors: true });
}
catch (e) {
throw new CustomErrors_1.IncompleteOpsYml(e);
}
};