UNPKG

@cto.ai/ops

Version:

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

385 lines (384 loc) • 19.6 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"); class Publish extends base_1.default { constructor() { super(...arguments); this.resolvePath = async (opPath) => { return path.resolve(process.cwd(), opPath); }; this.checkDocker = async (opPath) => { const docker = await get_docker_1.default(this, 'publish'); if (!docker) { throw new Error('No docker'); } return { opPath, docker }; }; this.getOpsAndWorkFlows = async (inputs) => { const { opPath } = inputs; 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.NoLocalOpsFound(); const { ops, version, workflows } = utils_1.parseYaml(manifest); if (!ops && !workflows) { throw new CustomErrors_1.NoLocalOpsOrWorkflowsFound(); } return Object.assign(Object.assign({}, inputs), { opCommands: ops, opWorkflows: workflows, version }); }; this.determineQuestions = async (inputs) => { const { opCommands, opWorkflows } = inputs; let opsAndWorkflows; if (opCommands && opCommands.length && opWorkflows && opWorkflows.length) { ; ({ opsAndWorkflows } = await sdk_1.ux.prompt({ type: 'list', name: 'opsAndWorkflows', message: `\n Which would you like to publish ${sdk_1.ux.colors.reset.green('→')}`, choices: [ { name: 'Commands', value: opConfig_1.COMMAND }, { name: 'Workflows', value: opConfig_1.WORKFLOW }, 'Both', ], afterMessage: `${sdk_1.ux.colors.reset.green('āœ“')}`, })); } else if (!opCommands || !opCommands.length) { opsAndWorkflows = opConfig_1.WORKFLOW; } else { opsAndWorkflows = opConfig_1.COMMAND; } return Object.assign(Object.assign({}, inputs), { commandsAndWorkflows: opsAndWorkflows }); }; this.selectOpsAndWorkFlows = async (inputs) => { let { commandsAndWorkflows, opCommands, opWorkflows } = inputs; switch (commandsAndWorkflows) { case opConfig_1.COMMAND: opCommands = await this.selectOps(opCommands); break; case opConfig_1.WORKFLOW: opWorkflows = await this.selectWorkflows(opWorkflows); break; default: opCommands = await this.selectOps(opCommands); opWorkflows = await this.selectWorkflows(opWorkflows); } return Object.assign(Object.assign({}, inputs), { opCommands, opWorkflows, commandsAndWorkflows }); }; 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.findOpsWhereVersionAlreadyExists = async (inputs) => { const { existingVersions: existingCommandVersions, filteredOps: opCommands, } = await this.filterExistingOps(inputs.opCommands); const { existingVersions: existingWorkflowVersions, filteredOps: opWorkflows, } = await this.filterExistingOps(inputs.opWorkflows); return Object.assign(Object.assign({}, inputs), { opCommands, opWorkflows, existingVersions: [ ...existingCommandVersions, ...existingWorkflowVersions, ] }); }; this.filterExistingOps = async (ops) => { let filteredOps = []; let existingVersions = []; for (const op of ops) { try { await this.services.api.find(`/private/teams/${this.team.name}/ops/${op.name}/versions/${op.version}`, { headers: { Authorization: this.accessToken, }, }); existingVersions = existingVersions.concat(op); } catch (err) { if (err.error[0].code === 404) { filteredOps = filteredOps.concat(op); continue; } throw new CustomErrors_1.APIError(err); } } return { existingVersions, filteredOps }; }; this.getNewVersion = async (inputs) => { if (inputs.existingVersions.length === 0) return inputs; let manifest = await fs.readFile(path.join(inputs.opPath, opConfig_1.OP_FILE), 'utf8'); this.log('\n šŸ¤” It seems like the version of the op that you are trying to publish already taken. \n Add a new version indicator in order to publish'); for (let existingOp of inputs.existingVersions) { this.log(`${this.ux.colors.callOutCyan(`Current version for ${existingOp.name}:`)} ${this.ux.colors.white(existingOp.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) => { try { 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'; } await this.services.api.find(`/private/teams/${this.team.name}/ops/${existingOp.name}/versions/${input}`, { headers: { Authorization: this.accessToken, }, }); return 'That version is already taken'; } catch (err) { if (err.error[0].code === 404) { return true; } throw new CustomErrors_1.APIError(err); } }, }); manifest = manifest.replace(`name: ${existingOp.name}:${existingOp.version}`, `name: ${existingOp.name}:${newVersion}`); existingOp.version = newVersion; if (existingOp.type === opConfig_1.COMMAND_TYPE) { inputs.opCommands = inputs.opCommands.concat(existingOp); const opImageTag = utils_1.getOpImageTag(this.team.name, existingOp.name, existingOp.version, existingOp.isPublic); const image = utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag); await this.services.imageService.build(image, path.resolve(process.cwd(), inputs.opPath), existingOp); } else if (existingOp.type === opConfig_1.WORKFLOW_TYPE) { inputs.opWorkflows = inputs.opWorkflows.concat(existingOp); } } fs.writeFileSync(path.join(inputs.opPath, opConfig_1.OP_FILE), manifest); return Object.assign({}, inputs); }; 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.publishOpsAndWorkflows = async (inputs) => { switch (inputs.commandsAndWorkflows) { case opConfig_1.COMMAND: await this.opsPublishLoop(inputs); break; case opConfig_1.WORKFLOW: await this.workflowsPublishLoop(inputs); break; default: await this.opsPublishLoop(inputs); await this.workflowsPublishLoop(inputs); } }; this.opsPublishLoop = async ({ opCommands, version }) => { try { for (const op of opCommands) { if (!validate_1.isValidOpName(op.name)) { throw new CustomErrors_1.InvalidInputCharacter('Op Name'); } if (!validate_1.isValidOpVersion(op)) { throw new CustomErrors_1.InvalidOpVersionFormat(); } const { publishDescription } = await this.ux.prompt({ type: 'input', name: 'publishDescription', message: `\nProvide a changelog of what's new for ${op.name}:${op.version} ${sdk_1.ux.colors.reset.green('→')}\n\n ${sdk_1.ux.colors.white('āœļø Changelog:')}`, 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); } if ('run' in op) { op.type = opConfig_1.COMMAND_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.registryAuthService, this.services.api, version); this.sendAnalytics('op', apiOp); } } } catch (err) { if (err instanceof ErrorTemplate_1.ErrorTemplate) { throw err; } throw new CustomErrors_1.APIError(err); 4; } }; this.workflowsPublishLoop = async ({ opWorkflows, version }) => { try { for (const workflow of opWorkflows) { if (!validate_1.isValidOpName(workflow.name)) { throw new CustomErrors_1.InvalidInputCharacter('Workflow Name'); } if (!validate_1.isValidOpVersion(workflow)) { 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) { const newSteps = []; for (const step of workflow.steps) { let newStep = ''; if (await this.services.buildStepService.isGlueCode(step)) { const opPath = path.resolve(__dirname, './../templates/workflowsteps/js/'); newStep = await this.services.buildStepService.buildAndPublishGlueCode(step, this.team.id, this.team.name, this.accessToken, opPath, this.user, this.services.publishService, this.services.opService, this.services.api, this.services.registryAuthService, this.state.config, workflow.isPublic, version); newSteps.push(newStep); } else { if (!this.services.buildStepService.isOpRun(step)) { this.debug('InvalidStepsFound - Step:', step); throw new CustomErrors_1.InvalidStepsFound(step); } newSteps.push(step); } } workflow.steps = newSteps; } 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); } 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) => { this.services.analytics.track({ userId: this.user.email, teamId: this.team.id, cliEvent: 'Ops CLI Publish', event: 'Ops CLI Publish', properties: { name: opOrWorkflow.name, team: this.team.name, namespace: `@${this.team.name}/${opOrWorkflow.name}`, email: this.user.email, username: this.user.username, type: publishType, description: opOrWorkflow.description, image: `${env_1.OPS_REGISTRY_HOST}/${opOrWorkflow.id.toLowerCase()}:${opOrWorkflow.version}`, tag: opOrWorkflow.version, }, }); }; } _validateDescription(input) { if (input === '') return 'You need to provide a publish description of your op before continuing'; return true; } async run() { try { await this.isLoggedIn(); const { args } = this.parse(Publish); const publishPipeline = utils_1.asyncPipe(this.resolvePath, this.checkDocker, this.getOpsAndWorkFlows, this.determineQuestions, this.selectOpsAndWorkFlows, this.findOpsWhereVersionAlreadyExists, this.getNewVersion, this.publishOpsAndWorkflows); await publishPipeline(args.path); } catch (err) { this.debug('%O', err); this.config.runHook('error', { err, accessToken: this.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, }, ];