UNPKG

@cto.ai/ops-rc

Version:

šŸ’» CTO.ai Ops - The CLI built for Teams šŸš€

413 lines (412 loc) • 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const sdk_1 = require("@cto.ai/sdk"); const fs = tslib_1.__importStar(require("fs-extra")); const path = tslib_1.__importStar(require("path")); const base_1 = tslib_1.__importStar(require("../base")); const env_1 = require("../constants/env"); const opConfig_1 = require("../constants/opConfig"); const CustomErrors_1 = require("./../errors/CustomErrors"); const utils_1 = require("./../utils"); const get_docker_1 = tslib_1.__importDefault(require("../utils/get-docker")); const validate_1 = require("../utils/validate"); const ErrorTemplate_1 = require("./../errors/ErrorTemplate"); const OpsYml_1 = require("./../types/OpsYml"); class Publish extends base_1.default { constructor() { super(...arguments); this.resolvePath = (opPath) => { return path.resolve(process.cwd(), opPath); }; this.checkDocker = async () => { const docker = await get_docker_1.default(this, 'publish'); if (!docker) { throw new Error('No docker'); } }; this.getOpsAndWorkflows = async (opPath) => { const manifest = await fs .readFile(path.join(opPath, opConfig_1.OP_FILE), 'utf8') .catch((err) => { this.debug('%O', err); throw new CustomErrors_1.FileNotFoundError(err, opPath, opConfig_1.OP_FILE); }); if (!manifest) throw new CustomErrors_1.NoLocalOpsOrPipelinesFound(); const parsedManifest = utils_1.parseYaml(manifest); const totalItems = parsedManifest.ops.length + parsedManifest.workflows.length + parsedManifest.pipelines.length + parsedManifest.services.length; if (totalItems === 0) { //TODO change error throw new CustomErrors_1.NoLocalOpsOrWorkflowsFound(); } return parsedManifest; }; this.convertPipelinesToWorkflows = (pipelines) => { const pipelineCommands = []; const pipelineWorkflows = pipelines.map(pipeline => { const runSteps = pipeline.jobs.map(({ name }) => { return 'ops run ' + name + ':' + pipeline.version; }); const newWorkflow = { name: pipeline.name, version: pipeline.version, description: 'auto generated pipeline job description for: ' + pipeline.name + ' ', type: opConfig_1.WORKFLOW_TYPE, remote: true, steps: runSteps, }; //convert each pipeline job to a OpCommand pipelineCommands.push(...pipeline.jobs.map(({ name }) => { let { ops } = utils_1.parseYaml(fs.readFileSync(`/tmp/${name}/ops.yml`, 'utf8')); return ops[0]; })); return newWorkflow; }); return { pipelineWorkflows, pipelineCommands }; }; this.choosePublishKind = async (commands, pipelines) => { if (commands.length !== 0 && pipelines.length !== 0) { const { commandsOrPipelines } = await sdk_1.ux.prompt({ type: 'list', name: 'commandsOrPipelines', message: `\n Which would you like to publish ${sdk_1.ux.colors.reset.green('→')}`, choices: [ { name: 'Commands', value: opConfig_1.COMMAND }, { name: 'Pipelines', value: opConfig_1.PIPELINE }, ], afterMessage: `${sdk_1.ux.colors.reset.green('āœ“')}`, }); return commandsOrPipelines; } else if (commands && commands.length) { return opConfig_1.COMMAND; } else if (pipelines && pipelines.length) { return opConfig_1.PIPELINE; } else { this.debug('found neither valid commands or pipelines'); process.exit(1); } }; this.selectOpsAndWorkflows = async (commandsOrPipelines, commands, workflows) => { switch (commandsOrPipelines) { case opConfig_1.COMMAND: const selectedCommands = await this.selectOps(commands); return { selectedCommands, selectedWorkflows: [] }; case opConfig_1.PIPELINE: const selectedWorkflows = await this.selectWorkflows(workflows); return { selectedCommands: [], selectedWorkflows }; default: this.debug('found neither valid commands or pipelines'); process.exit(1); } }; this.selectOps = async (ops) => { if (ops.length <= 1) { return ops; } const answers = await sdk_1.ux.prompt({ type: 'checkbox', name: 'ops', message: `\n Which ops would you like to publish ${sdk_1.ux.colors.reset.green('→')}`, choices: ops.map(op => { return { value: op, name: `${op.name} - ${op.description}`, }; }), validate: input => input.length > 0, }); return answers.ops; }; this.selectWorkflows = async (workflows) => { if (workflows.length <= 1) { return workflows; } const answers = await sdk_1.ux.prompt({ type: 'checkbox', name: 'workflows', message: `\n Which workflows would you like to publish ${sdk_1.ux.colors.reset.green('→')}`, choices: workflows.map(workflow => { return { value: workflow, name: `${workflow.name} - ${workflow.description}`, }; }), validate: input => input.length > 0, }); return answers.workflows; }; this.checkForExistingVersion = async (name, version) => { try { await this.services.api.find(`/private/teams/${this.team.name}/ops/${name}/versions/${version}`, { headers: { Authorization: this.accessToken, }, }); return true; } catch (err) { if (err.error[0].code === 404) { return false; } throw new CustomErrors_1.APIError(err); } }; this.ensureAvailableVersions = async (opPath, commandsOrPipelines, commands, workflows) => { if (commandsOrPipelines === opConfig_1.PIPELINE) { for (const workflow of workflows) { if (await this.checkForExistingVersion(workflow.name, workflow.version)) { this.log('\n šŸ¤” It seems like the version of the pipeline that you are trying to publish already taken. \n Please update the pipeline version in the ops.yml, rebuild and try again.'); process.exit(1); } } return { commands, workflows }; } // Going to assume we're doing commands const updatedCommands = []; let manifest = await fs.readFile(path.join(opPath, opConfig_1.OP_FILE), 'utf8'); for (const command of commands) { // Ignore anything that isn't existing if (!(await this.checkForExistingVersion(command.name, command.version))) { updatedCommands.push(command); continue; } this.log(`${this.ux.colors.callOutCyan(`Current version for ${command.name}:`)} ${this.ux.colors.white(command.version)}`); const { newVersion } = await this.ux.prompt({ type: 'input', name: 'newVersion', message: '\nāœļø Update version:', transformer: input => { return this.ux.colors.white(input); }, validate: async (input) => { if (input === '') return 'Please enter a version'; if (!validate_1.validVersionChars.test(input)) { return 'ā— Sorry, version is required and can only contain letters, digits, underscores, \n periods and dashes and must start and end with a letter or a digit'; } if (await this.checkForExistingVersion(command.name, input)) { return 'That version is already taken'; } return true; }, }); manifest = manifest.replace(`name: ${command.name}:${command.version}`, `name: ${command.name}:${newVersion}`); command.version = newVersion; if (command.type === opConfig_1.COMMAND_TYPE || command.type === opConfig_1.SERVICE_TYPE) { const opImageTag = utils_1.getOpImageTag(this.team.name, command.name, command.version, command.isPublic); const image = utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag); await this.services.imageService.build(image, path.resolve(process.cwd(), opPath), command); } updatedCommands.push(command); } return { commands: updatedCommands, workflows }; }; this.getRegistryAuth = async (name, version) => { try { const registryAuth = await this.services.registryAuthService.create(this.accessToken, this.team.name, name, version, false, true); return registryAuth; } catch (err) { throw new CustomErrors_1.CouldNotGetRegistryToken(err); } }; this.pipelineCommandsPublishLoop = async (pipelineCommands, version, config) => { for (const op of pipelineCommands) { if (!validate_1.isValidOpName(op.name)) { throw new CustomErrors_1.InvalidInputCharacter('Op Name'); } if (!validate_1.isValidOpVersion(op.version)) { throw new CustomErrors_1.InvalidOpVersionFormat(); } op.publishDescription = 'auto generated publish description for pipeline job ' + op.name; try { const opName = utils_1.getOpImageTag(this.team.name, op.name, op.version, op.isPublic); const localImage = await this.services.imageService.checkLocalImage(`${env_1.OPS_REGISTRY_HOST}/${opName}`); if (!localImage) { throw new CustomErrors_1.DockerPublishNoImageFound(op.name, this.team.name); } op.type = opConfig_1.JOB_TYPE; const { data: apiOp, } = await this.services.publishService.publishOpToAPI(op, version, this.team.name, this.accessToken, this.services.api); const registryAuth = await this.getRegistryAuth(op.name, op.version); await this.services.publishService.publishOpToRegistry(apiOp, registryAuth, this.team.name, this.accessToken, this.services.api); this.sendAnalytics('op', apiOp, config); } catch (err) { if (err instanceof ErrorTemplate_1.ErrorTemplate) { throw err; } throw new CustomErrors_1.APIError(err); } } }; this.opsPublishLoop = async (opCommands, version, config) => { try { for (const op of opCommands) { if (!validate_1.isValidOpName(op.name)) { throw new CustomErrors_1.InvalidInputCharacter('Op Name'); } if (!validate_1.isValidOpVersion(op.version)) { throw new CustomErrors_1.InvalidOpVersionFormat(); } const { publishDescription } = await this.ux.prompt({ type: 'input', name: 'publishDescription', message: `\nProvide a publish description for ${op.name}:${op.version} ${sdk_1.ux.colors.reset.green('→')}\n\n ${sdk_1.ux.colors.white('Description:')}`, afterMessage: sdk_1.ux.colors.reset.green('āœ“'), afterMessageAppend: sdk_1.ux.colors.reset(' added!'), validate: this._validateDescription, }); op.publishDescription = publishDescription; const opName = utils_1.getOpImageTag(this.team.name, op.name, op.version, op.isPublic); const localImage = await this.services.imageService.checkLocalImage(`${env_1.OPS_REGISTRY_HOST}/${opName}`); if (!localImage) { throw new CustomErrors_1.DockerPublishNoImageFound(op.name, this.team.name); } // TODO: What do we do if this isn't true if ('run' in op) { const { data: apiOp, } = await this.services.publishService.publishOpToAPI(op, version, this.team.name, this.accessToken, this.services.api); const registryAuth = await this.getRegistryAuth(op.name, op.version); await this.services.publishService.publishOpToRegistry(apiOp, registryAuth, this.team.name, this.accessToken, this.services.api); this.sendAnalytics('op', apiOp, config); } } } catch (err) { if (err instanceof ErrorTemplate_1.ErrorTemplate) { throw err; } throw new CustomErrors_1.APIError(err); } }; this.workflowsPublishLoop = async (opWorkflows, version, config) => { try { for (const workflow of opWorkflows) { if (!validate_1.isValidOpName(workflow.name)) { throw new CustomErrors_1.InvalidInputCharacter('Workflow Name'); } if (!validate_1.isValidOpVersion(workflow.version)) { throw new CustomErrors_1.InvalidOpVersionFormat(); } const { publishDescription } = await this.ux.prompt({ type: 'input', name: 'publishDescription', message: `\nProvide a publish description for ${workflow.name}:${workflow.version} ${sdk_1.ux.colors.reset.green('→')}\n\n ${sdk_1.ux.colors.white('Description:')}`, afterMessage: sdk_1.ux.colors.reset.green('āœ“'), afterMessageAppend: sdk_1.ux.colors.reset(' added!'), validate: this._validateDescription, }); workflow.publishDescription = publishDescription; if ('remote' in workflow && workflow.remote) { for (const step of workflow.steps) { if (!step.includes('ops run')) { this.debug('InvalidStepsFound - Step:', step); throw new CustomErrors_1.InvalidStepsFound(step); } } } try { const { data: apiWorkflow, } = await this.services.api.create(`/private/teams/${this.team.name}/ops`, Object.assign(Object.assign({}, workflow), { platformVersion: version, type: 'workflow' }), { headers: { Authorization: this.accessToken, }, }); this.log(`\nšŸ™Œ ${sdk_1.ux.colors.callOutCyan(apiWorkflow.name)} has been published!`); this.log(`šŸ–„ Visit your Op page here: ${sdk_1.ux.url(`${env_1.OPS_API_HOST}registry/${this.team.name}/${apiWorkflow.name}`, `<${env_1.OPS_API_HOST}${this.team.name}/${apiWorkflow.name}>`)}\n`); this.sendAnalytics('workflow', apiWorkflow, config); } catch (err) { this.debug('%O', err); const InvalidWorkflowStepCodes = [400, 404]; if (err && err.error && err.error[0] && InvalidWorkflowStepCodes.includes(err.error[0].code)) { if (err.error[0].message === 'version is taken') { throw new CustomErrors_1.VersionIsTaken(); } throw new CustomErrors_1.InvalidWorkflowStep(err); } throw new CustomErrors_1.CouldNotCreateWorkflow(err.message); } } } catch (err) { if (err instanceof ErrorTemplate_1.ErrorTemplate) throw err; throw new CustomErrors_1.APIError(err); } }; this.sendAnalytics = (publishType, opOrWorkflow, config) => { this.services.analytics.track('Ops CLI Publish', { name: opOrWorkflow.name, team: config.team.name, namespace: `@${config.team.name}/${opOrWorkflow.name}`, username: config.user.username, type: publishType, description: opOrWorkflow.description, image: `${env_1.OPS_REGISTRY_HOST}/${opOrWorkflow.id.toLowerCase()}:${opOrWorkflow.version}`, tag: opOrWorkflow.version, }, config); }; } _validateDescription(input) { if (input === '') return 'You need to provide a publish description of your op before continuing'; return true; } async run() { const config = await this.isLoggedIn(); try { const { args } = this.parse(Publish); await this.checkDocker(); const opPath = this.resolvePath(args.path); const manifest = await this.getOpsAndWorkflows(opPath); const { pipelineWorkflows, pipelineCommands, } = this.convertPipelinesToWorkflows(manifest.pipelines); const serviceOps = OpsYml_1.convertServicesToOps(manifest.services); const commands = manifest.ops.concat(serviceOps); const workflows = manifest.workflows.concat(pipelineWorkflows); const commandsOrPipelines = await this.choosePublishKind(commands, manifest.pipelines); const { selectedCommands, selectedWorkflows, } = await this.selectOpsAndWorkflows(commandsOrPipelines, commands, workflows); const { commands: finalCommands, workflows: finalWorkflows, } = await this.ensureAvailableVersions(opPath, commandsOrPipelines, selectedCommands, selectedWorkflows); switch (commandsOrPipelines) { case opConfig_1.COMMAND: await this.opsPublishLoop(finalCommands, manifest.version, config); break; case opConfig_1.PIPELINE: await this.pipelineCommandsPublishLoop(pipelineCommands, manifest.version, config); await this.workflowsPublishLoop(finalWorkflows, manifest.version, config); break; default: this.debug('found neither valid commands or pipelines'); process.exit(1); } } catch (err) { this.debug('%O', err); this.config.runHook('error', { err, accessToken: config.tokens.accessToken, }); } } } exports.default = Publish; Publish.description = 'Publish an Op to your team.'; Publish.flags = { help: base_1.flags.help({ char: 'h' }), }; Publish.args = [ { name: 'path', description: 'Path to the op you want to publish.', required: true, }, ];