UNPKG

@cto.ai/ops

Version:

💻 CTO.ai - The CLI built for Teams 🚀

262 lines (261 loc) • 11.9 kB
"use strict"; 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); } };