diff --git a/packages/teraslice-cli/package.json b/packages/teraslice-cli/package.json index a46cee1f46d..905fbde3d93 100644 --- a/packages/teraslice-cli/package.json +++ b/packages/teraslice-cli/package.json @@ -1,7 +1,7 @@ { "name": "teraslice-cli", "displayName": "Teraslice CLI", - "version": "2.1.0", + "version": "2.2.0", "description": "Command line manager for teraslice jobs, assets, and cluster references.", "keywords": [ "teraslice" @@ -42,6 +42,7 @@ "@terascope/utils": "^0.60.0", "chalk": "^4.1.2", "cli-table3": "^0.6.4", + "diff": "^5.2.0", "easy-table": "^1.2.0", "ejs": "^3.1.10", "esbuild": "^0.21.5", @@ -62,6 +63,7 @@ }, "devDependencies": { "@types/decompress": "^4.2.7", + "@types/diff": "^5.2.1", "@types/easy-table": "^1.2.0", "@types/ejs": "^3.1.5", "@types/js-yaml": "^4.0.9", diff --git a/packages/teraslice-cli/src/cmds/tjm/view.ts b/packages/teraslice-cli/src/cmds/tjm/view.ts index c30bfb2f3f1..b23216ea957 100644 --- a/packages/teraslice-cli/src/cmds/tjm/view.ts +++ b/packages/teraslice-cli/src/cmds/tjm/view.ts @@ -14,8 +14,10 @@ export default { yargs.option('src-dir', yargsOptions.buildOption('src-dir')); yargs.option('config-dir', yargsOptions.buildOption('config-dir')); yargs.options('status', yargsOptions.buildOption('jobs-status')); + yargs.option('diff', yargsOptions.buildOption('diff')); yargs .example('$0 tjm view JOB_FILE.json', 'displays job config on job cluster') + .example('$0 tjm view JOB_FILE.json --diff', 'Shows diff between the local json file and whats currently on the cluster') .example('$0 tjm view JOB_FILE1.json JOB_FILE2.json', 'displays config for multiple job files'); return yargs; }, diff --git a/packages/teraslice-cli/src/helpers/jobs.ts b/packages/teraslice-cli/src/helpers/jobs.ts index 5c85af37a63..63f498ffcd8 100644 --- a/packages/teraslice-cli/src/helpers/jobs.ts +++ b/packages/teraslice-cli/src/helpers/jobs.ts @@ -3,6 +3,9 @@ import { has, toString, pDelay, pMap, } from '@terascope/utils'; import { Teraslice } from '@terascope/types'; +import chalk from 'chalk'; +import * as diff from 'diff'; +import path from 'node:path'; import { Job } from 'teraslice-client-js'; import TerasliceUtil from './teraslice-util.js'; import Display from './display.js'; @@ -669,6 +672,88 @@ export default class Jobs { } } + formatJobConfig(jobConfig: JobConfigFile) { + const finalJobConfig: Partial = {}; + Object.keys(jobConfig).forEach((key) => { + if (key === '__metadata') { + finalJobConfig.job_id = jobConfig[key].cli.job_id; + finalJobConfig._updated = jobConfig[key].cli.updated; + } else { + finalJobConfig[key] = jobConfig[key]; + } + }); + return finalJobConfig; + } + + getLocalJSONConfigs(srcDir: string, files: string[]) { + const localJobConfigs = {}; + for (const file of files) { + const filePath = path.join(srcDir, file); + const jobConfig: JobConfigFile = JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' })); + const formattedJobConfig = this.formatJobConfig(jobConfig); + localJobConfigs[formattedJobConfig.job_id as string] = formattedJobConfig; + } + return localJobConfigs; + } + + printDiff(diffResult: Diff.Change[], showUpdateField: boolean) { + diffResult.forEach((part) => { + let color: chalk.Chalk; + let symbol: string; + let pointer: string; + if (part.added) { + color = chalk.green; + symbol = '+'; + pointer = ' <--- local job file value'; + } else if (part.removed) { + color = chalk.red; + symbol = '-'; + pointer = ' <--- state cluster value'; + } else { + color = chalk.grey; + symbol = ' '; + pointer = ''; + } + const lines = part.value.split('\n'); + lines.forEach((line) => { + /// Don't print blank lines + if (line.length !== 0) { + /// These fields aren't in the job file so don't compare in diff + if (!line.includes('"_created":') && !line.includes('"_context":')) { + /// Check to see if we want to display _updated field + if (line.includes('"_updated":')) { + if (showUpdateField) { + process.stdout.write(color(`${symbol} ${line}${pointer}\n`)); + } + } else { + process.stdout.write(color(`${symbol} ${line}${pointer}\n`)); + } + } + } + }); + }); + } + + getJobDiff(job: JobMetadata) { + const localJobConfigs = this.getLocalJSONConfigs( + this.config.args.srcDir, + this.config.args.jobFile + ); + const diffObject = diff.diffJson(job.config, localJobConfigs[job.id]); + + /// "_update" fields on the job file are always off by a couple milliseconds + /// We only want to display a diff of this field if it's greater than a minute + let showUpdateField = false; + const jobConfigUpdateTime = new Date(job.config._updated).getTime(); + const localConfigUpdateTime = new Date(localJobConfigs[job.id]._updated).getTime(); + const timeDiff = Math.abs(localConfigUpdateTime - jobConfigUpdateTime); + if (timeDiff > (1000 * 60)) { + showUpdateField = true; + } + + this.printDiff(diffObject, showUpdateField); + } + /** * @param args action and final property, final indicates if it is part of a series of commands * @param job job metadata @@ -692,7 +777,11 @@ export default class Jobs { ({ message, final } = this.getUpdateMessage(action, job)); } - reply.yellow(`> ${message}`); + if (this.config.args.diff && action === 'view') { + this.getJobDiff(job); + } else { + reply.yellow(`> ${message}`); + } if (final) { reply.green(`${jobInfoString}`); diff --git a/packages/teraslice-cli/src/helpers/yargs-options.ts b/packages/teraslice-cli/src/helpers/yargs-options.ts index 0158fd0a7d7..01e8aac7c48 100644 --- a/packages/teraslice-cli/src/helpers/yargs-options.ts +++ b/packages/teraslice-cli/src/helpers/yargs-options.ts @@ -228,6 +228,11 @@ export default class Options { describe: 'Silence non-error logging.', type: 'boolean' }), + diff: () => ({ + describe: 'Shows diff on a job in a cluster and a job file', + default: false, + type: 'boolean' + }) }; private positionals: Record yargs.PositionalOptions> = { diff --git a/yarn.lock b/yarn.lock index c5431773570..b180c81163d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2682,6 +2682,11 @@ dependencies: "@types/node" "*" +"@types/diff@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.2.1.tgz#cceae9c4b2dae5c6b8ab1ce1263601c255d87fb3" + integrity sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g== + "@types/easy-table@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/easy-table/-/easy-table-1.2.0.tgz#d7153551a2c3f6571dddff974b05aa2fb1a4a948" @@ -5214,7 +5219,7 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^5.0.0: +diff@^5.0.0, diff@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==