diff --git a/bin/cortex-deploy.js b/bin/cortex-deploy.js new file mode 100755 index 00000000..d9bc8505 --- /dev/null +++ b/bin/cortex-deploy.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +/* + * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the “License”); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const chalk = require('chalk'); +const program = require('../src/commander'); + +const { withCompatibilityCheck } = require('../src/compatibility'); + +const { + DeploySnapshotCommand +} = require('../src/commands/deploy'); + +program.description('Export Cortex artifacts for deployment'); + +// Export Agent Snapshot. Provide snapshotIds in quotes separated by space like "snapshotId1 snapshotId2". This is to support multiple agents in one deployment manifest file. +program + .command('snapshots ') + .description('Export Agent(s) snapshots(s) for deployment. Provide snapshotIds separated by space, as <"snapshotId1 snapshotId2 ...">') + .option('--no-compat', 'Ignore API compatibility checks') + .option('--color [on/off]', 'Turn on/off colors for JSON output.', 'on') + .option('--profile [profile]', 'The profile to use') + .option('-y, --yaml', 'Use YAML for snapshot export format') + .option('-f, --force', 'Force delete existing exported files') + .action(withCompatibilityCheck((skillDefinition, options) => { + try { + new DeploySnapshotCommand(program).execute(skillDefinition, options); + } + catch (err) { + console.error(chalk.red(err.message)); + } + })); + +program.parse(process.argv); diff --git a/bin/cortex.js b/bin/cortex.js index f27ca511..9bd707bb 100755 --- a/bin/cortex.js +++ b/bin/cortex.js @@ -33,6 +33,7 @@ program .command('connections [cmd]', 'Work with Cortex Connections') .command('content [cmd]', 'Work with Cortex Managed Content') .command('environments [cmd]', 'Work with Cortex Environments') + .command('deploy [cmd]', 'Work with Cortex Artifacts export for deployment') .command('datasets [cmd]', 'Work with Cortex Datasets') .command('docker [cmd]', 'Work with Docker') .command('jobs [cmd]', 'Work with Cortex v2 Jobs') diff --git a/src/commands/deploy.js b/src/commands/deploy.js new file mode 100644 index 00000000..a4cfbc1c --- /dev/null +++ b/src/commands/deploy.js @@ -0,0 +1,103 @@ +/* + * Copyright 2018 Cognitive Scale, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the “License”); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an “AS IS” BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const debug = require('debug')('cortex:cli'); +const {loadProfile} = require('../config'); +const path = require('path'); +const Agents = require('../client/agents'); +const {printSuccess, printError, filterObject, cleanInternalFields, jsonToYaml, writeToFile, fileExists, deleteFile} = require('./utils'); + + +/** + * Cortex deploy command is to export Cortex artifacts (agent, snapshot, skill, action etc) for CI/CD deployment. This command: + * 1. Exports cortex artifacts + * 2. generates manifest files to drive artifacts deployment + * Files are exported/generated in pre-determined layout to support CI/CD deployment + * .fabric + * snapshots + * snapshot1.yaml + * snapshot2.yaml + * agents + * ... + * skills + * ... + * actions + * ... + * fabric.yaml (manifest file) + * + * Currently only exporting agent snapshots. + * + * @type {DeploySnapshotCommand} + */ +module.exports.DeploySnapshotCommand = class { + constructor(program) { + this.program = program; + } + + execute(snapshotIds, options) { + const exportPath = '.fabric'; + const manifestFile = 'fabric.yaml'; + + if (fileExists(exportPath) || fileExists(manifestFile)) { + if (options.force) { + deleteFile(exportPath); + deleteFile(manifestFile); + printSuccess(`Deleted ${exportPath} and ${manifestFile}`); + } else { + printError(`Aborting, because export path ${exportPath} or manifest file ${manifestFile} exists. Use -f option to force delete.`); + } + } + const profile = loadProfile(options.profile); + const envName = options.environmentName; + debug('%s.exportDeploymentSnapshot(%s)', profile.name, snapshotIds); + + const agents = new Agents(profile.url); + const promises = []; + snapshotIds.split(' ').forEach(function (snapshotId) { + promises.push(agents.describeAgentSnapshot(profile.token, snapshotId, envName).then((response) => { + if (response.success) { + let result = filterObject(response.result, options); + result = cleanInternalFields(result); + //TODO add validation for not exporting tip snapshots, as they don't have all dependencies. Should we enforce? + let filename = snapshotId + ".json"; + if (options.yaml) { + result = jsonToYaml(result); + filename = snapshotId + ".yaml"; + } + const filepath = path.join(exportPath, 'snapshots', filename); + writeToFile(result, filepath); + printSuccess(`Successfully exported agent snapshot ${filepath}`); + return filepath; + } else { + printError(`Failed to export agent snapshot ${snapshotId}: ${response.message}`, options); + } + }).catch((err) => { + printError(`Failed to export agent snapshot ${snapshotId}: ${err.status} ${err.message}`, options); + })); + }); + + Promise.all(promises).then(result => { + const manifest = { + "version": 1, + "kind": "deployment-manifest", + "cortex": { + "snapshots": result + } + }; + writeToFile(jsonToYaml(manifest), manifestFile); + printSuccess(`Successfully generated manifest file ${manifestFile}`); + }); + } +}; diff --git a/src/commands/utils.js b/src/commands/utils.js index 6da9413d..7d7d8385 100644 --- a/src/commands/utils.js +++ b/src/commands/utils.js @@ -84,7 +84,7 @@ module.exports.parseObject = function(str, options) { if (options.yaml) { return yaml.safeLoad(str); } - + return JSON.parse(str); }; @@ -195,7 +195,7 @@ module.exports.formatAllServiceInputParameters = function(allParameters){ return `$ref:${allParameters.$ref}`; } else{ - return allParameters.map(inputParameters => formatServiceInputParameter(inputParameters)).join('\n'); + return allParameters.map(inputParameters => formatServiceInputParameter(inputParameters)).join('\n'); } } @@ -246,3 +246,57 @@ module.exports.formatValidationPath = (p) => { }); return res; }; + +module.exports.cleanInternalFields = function(jsobj) { + return JSON.stringify(jsobj, function re(a, obj) { + if (a.startsWith("_")) { + return undefined; + } + return obj; + }, 2); +}; + +module.exports.jsonToYaml = function (json) { + if (typeof json == 'string') { + json = JSON.parse(json); + } + return yaml.dump(json); +}; + +module.exports.writeToFile = function (content, filepath) { + const dir = path.dirname(filepath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, {recursive: true}); + } + + fs.writeFileSync(filepath, content); +}; + +module.exports.fileExists = function (filepath) { + return fs.existsSync(filepath) +}; + +// Alternatively, we can use fs.rmdirSync(, {recursive: true}), but that requires node v12+ +const deleteFolderRecursive = function(filepath) { + try { + if (fs.existsSync(filepath)) { + if (fs.lstatSync(filepath).isDirectory()) { + fs.readdirSync(filepath).forEach((file, index) => { + const curPath = path.join(filepath, file); + if (fs.lstatSync(curPath).isDirectory()) { // recurse + deleteFolderRecursive(curPath); + } else { // delete file + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(filepath); + } else { + fs.unlinkSync(filepath); + } + } + } catch (e) { + console.error(chalk.red(e.message)); + } +}; + +module.exports.deleteFile = deleteFolderRecursive;