From 6bc008531edb78b759610075a73986d3e680054b Mon Sep 17 00:00:00 2001 From: Arthur Granado Date: Thu, 14 May 2020 13:35:48 +0100 Subject: [PATCH] feat: include command "container" as alias for option "--docker" This is the first step to create a "snyk container test" or "snyk container monitor" commands that doesn't require Docker engine running. Although it's not relevant to all users, more flexibility here is both more technically correct, future proofs our technology, and is important to several separate strategic partners. The option "--docker" is still available and the CLI is still compatible with this option. Future documentation improvement will give preference for the new command "container" rather than the option "--docker". --- src/cli/args.ts | 4 + src/cli/index.ts | 3 + src/cli/modes.ts | 66 ++++++++++++++ test/args.test.ts | 36 ++++++++ test/modes.test.ts | 212 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 321 insertions(+) create mode 100644 src/cli/modes.ts create mode 100644 test/modes.test.ts diff --git a/src/cli/args.ts b/src/cli/args.ts index bd1fa9b7f9..583880c566 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,6 +1,7 @@ import * as abbrev from 'abbrev'; import debugModule = require('debug'); +import { parseMode } from './modes'; export declare interface Global extends NodeJS.Global { ignoreUnknownCA: boolean; @@ -111,6 +112,9 @@ export function args(rawArgv: string[]): Args { // an argument to our command, like `snyk help protect` let command = argv._.shift() as string; // can actually be undefined + // snyk [mode?] [command] [paths?] [options-double-dash] + command = parseMode(command, argv); + // alias switcheroo - allows us to have if (cli.aliases[command]) { command = cli.aliases[command]; diff --git a/src/cli/index.ts b/src/cli/index.ts index 4d339255c7..eef8dbc2bb 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -25,6 +25,7 @@ import { } from '../lib/errors'; import stripAnsi from 'strip-ansi'; import { ExcludeFlagInvalidInputError } from '../lib/errors/exclude-flag-invalid-input'; +import { modeValidation } from './modes'; const debug = Debug('snyk'); const EXIT_CODES = { @@ -158,6 +159,8 @@ async function main() { let failed = false; let exitCode = EXIT_CODES.ERROR; try { + modeValidation(args); + if (args.options.scanAllUnmanaged && args.options.file) { throw new UnsupportedOptionCombinationError([ 'file', diff --git a/src/cli/modes.ts b/src/cli/modes.ts new file mode 100644 index 0000000000..02c3bfc833 --- /dev/null +++ b/src/cli/modes.ts @@ -0,0 +1,66 @@ +import * as abbrev from 'abbrev'; +import { UnsupportedOptionCombinationError, CustomError } from '../lib/errors'; + +interface ModeData { + allowedCommands: Array; + config: (args) => []; +} + +const modes: Record = { + container: { + allowedCommands: ['test', 'monitor'], + config: (args): [] => { + args['docker'] = true; + + return args; + }, + }, +}; + +export function parseMode(mode: string, args): string { + if (isValidMode(mode)) { + const command: string = args._[0]; + + if (isValidCommand(mode, command)) { + configArgs(mode, args); + mode = args._.shift(); + } + } + + return mode; +} + +export function modeValidation(args: object) { + const mode = args['command']; + const commands: Array = args['options']._; + + if (isValidMode(mode) && commands.length <= 1) { + const allowed = modes[mode].allowedCommands + .join(', ') + .replace(/, ([^,]*)$/, ' or $1'); + const message = `use snyk ${mode} with ${allowed}`; + + throw new CustomError(message); + } + + const command = commands[0]; + if (isValidMode(mode) && !isValidCommand(mode, command)) { + const notSupported = [mode, command]; + + throw new UnsupportedOptionCombinationError(notSupported); + } +} + +function isValidMode(mode: string): boolean { + return Object.keys(modes).includes(mode); +} + +function isValidCommand(mode: string, command: string): boolean { + const aliases = abbrev(modes[mode].allowedCommands); + + return Object.keys(aliases).includes(command); +} + +function configArgs(mode: string, args): [] { + return modes[mode].config(args); +} diff --git a/test/args.test.ts b/test/args.test.ts index f7f470ebae..1f35fe1e42 100644 --- a/test/args.test.ts +++ b/test/args.test.ts @@ -150,3 +150,39 @@ test('test command line test --container', (t) => { t.notOk(result.options.container); t.end(); }); + +test('test command line "container test"', (t) => { + const cliArgs = [ + '/Users/dror/.nvm/versions/node/v6.9.2/bin/node', + '/Users/dror/work/snyk/snyk-internal/cli', + 'container', + 'test', + ]; + const result = args(cliArgs); + t.ok(result.options.docker); + t.end(); +}); + +test('test command line "container monitor"', (t) => { + const cliArgs = [ + '/Users/dror/.nvm/versions/node/v6.9.2/bin/node', + '/Users/dror/work/snyk/snyk-internal/cli', + 'container', + 'monitor', + ]; + const result = args(cliArgs); + t.ok(result.options.docker); + t.end(); +}); + +test('test command line "container protect"', (t) => { + const cliArgs = [ + '/Users/dror/.nvm/versions/node/v6.9.2/bin/node', + '/Users/dror/work/snyk/snyk-internal/cli', + 'container', + 'protect', + ]; + const result = args(cliArgs); + t.notOk(result.options.docker); + t.end(); +}); diff --git a/test/modes.test.ts b/test/modes.test.ts new file mode 100644 index 0000000000..8f70ad1cec --- /dev/null +++ b/test/modes.test.ts @@ -0,0 +1,212 @@ +import { test } from 'tap'; +import { + UnsupportedOptionCombinationError, + CustomError, +} from '../src/lib/errors'; +import { parseMode, modeValidation } from '../src/cli/modes'; + +test('when is missing command', (c) => { + c.test('should do nothing', (t) => { + const cliCommand = 'container'; + const cliArgs = { + _: [], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, cliCommand); + t.equal(cliArgs, cliArgs); + t.notOk(cliArgs['docker']); + t.end(); + }); + c.end(); +}); + +test('when is not a valid mode', (c) => { + c.test('should do nothing', (t) => { + const cliCommand = 'test'; + const cliArgs = { + _: [], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, cliCommand); + t.equal(cliArgs, cliArgs); + t.notOk(cliArgs['docker']); + t.end(); + }); + c.end(); +}); + +test('when is a valid mode', (c) => { + c.test('when is allowed command', (d) => { + d.test( + '"container test" should set docker option and test command', + (t) => { + const expectedCommand = 'test'; + const expectedArgs = { + _: [], + docker: true, + 'package-manager': 'pip', + }; + const cliCommand = 'container'; + const cliArgs = { + _: ['test'], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, expectedCommand); + t.same(cliArgs, expectedArgs); + t.ok(cliArgs['docker']); + t.end(); + }, + ); + + d.test('when there is a command alias', (t) => { + t.test('"container t" should set docker option and test command', (t) => { + const expectedCommand = 't'; + const expectedArgs = { + _: [], + docker: true, + 'package-manager': 'pip', + }; + const cliCommand = 'container'; + const cliArgs = { + _: ['t'], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, expectedCommand); + t.same(cliArgs, expectedArgs); + t.ok(cliArgs['docker']); + t.end(); + }); + t.end(); + }); + d.end(); + }); + + c.test('when is not allowed command', (d) => { + d.test( + '"container protect" should not set docker option and return same command', + (t) => { + const expectedCommand = 'container'; + const expectedArgs = { + _: ['protect'], + 'package-manager': 'pip', + }; + const cliCommand = 'container'; + const cliArgs = { + _: ['protect'], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, expectedCommand); + t.same(cliArgs, expectedArgs); + t.notOk(cliArgs['docker']); + t.end(); + }, + ); + d.end(); + }); + + c.test('mode validation', (d) => { + d.test('when there is no command, throw error', (t) => { + const args = { + command: 'container', + options: { + _: ['container'], + }, + }; + + try { + modeValidation(args); + } catch (err) { + t.ok(err instanceof CustomError, 'should throw CustomError'); + t.equal( + err.message, + 'use snyk container with test or monitor', + 'should have error message', + ); + t.end(); + } + }); + + d.test('when command is not valid, throw error', (t) => { + const args = { + command: 'container', + options: { + _: ['protect', 'container'], + }, + }; + + try { + modeValidation(args); + } catch (err) { + t.ok( + err instanceof UnsupportedOptionCombinationError, + 'should throw UnsupportedOptionCombinationError', + ); + t.equal( + err.message, + 'The following option combination is not currently supported: container + protect', + 'should have error message', + ); + t.end(); + } + }); + + d.test('when command is valid, do nothing', (t) => { + const args = { + command: 'container', + options: { + _: ['test', 'container'], + }, + }; + + modeValidation(args); + + t.ok('should not throw error'); + t.end(); + }); + + d.test('when there is no valid mode, do nothing', (t) => { + const args = { + command: 'test', + options: { + _: ['test'], + }, + }; + + modeValidation(args); + + t.ok('should not throw error'); + t.end(); + }); + + d.test('when there is no mode, do nothing', (t) => { + const args = { + command: '', + options: { + _: [], + }, + }; + + modeValidation(args); + + t.ok('should not throw error'); + t.end(); + }); + d.end(); + }); + c.end(); +});