UI/console update (#20590)

This commit is contained in:
Chelsea Shaw 2023-05-17 11:41:02 -05:00 committed by GitHub
parent e58f3816a4
commit 722c578ff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 301 additions and 134 deletions

3
changelog/20590.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Update Web CLI with examples and a new `kv-get` command for reading kv v2 data and metadata
```

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@ember/component';
export default Component.extend({});

View File

@ -8,15 +8,17 @@ import { alias, or } from '@ember/object/computed';
import Component from '@ember/component'; import Component from '@ember/component';
import { getOwner } from '@ember/application'; import { getOwner } from '@ember/application';
import { schedule } from '@ember/runloop'; import { schedule } from '@ember/runloop';
import { camelize } from '@ember/string';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import ControlGroupError from 'vault/lib/control-group-error'; import ControlGroupError from 'vault/lib/control-group-error';
import { import {
parseCommand, parseCommand,
extractDataAndFlags,
logFromResponse, logFromResponse,
logFromError, logFromError,
logErrorFromInput, formattedErrorFromInput,
executeUICommand, executeUICommand,
extractFlagsFromStrings,
extractDataFromStrings,
} from 'vault/lib/console-helpers'; } from 'vault/lib/console-helpers';
export default Component.extend({ export default Component.extend({
@ -64,29 +66,25 @@ export default Component.extend({
// parse to verify it's valid // parse to verify it's valid
try { try {
serviceArgs = parseCommand(command, shouldThrow); serviceArgs = parseCommand(command);
} catch (e) { } catch (e) {
if (shouldThrow) {
this.logAndOutput(command, { type: 'help' }); this.logAndOutput(command, { type: 'help' });
return;
} }
// we have a invalid command but don't want to throw
if (serviceArgs === false) {
return; return;
} }
const [method, flagArray, path, dataArray] = serviceArgs; const { method, flagArray, path, dataArray } = serviceArgs;
const flags = extractFlagsFromStrings(flagArray, method);
const data = extractDataFromStrings(dataArray);
if (dataArray || flagArray) { const inputError = formattedErrorFromInput(path, method, flags, dataArray);
var { data, flags } = extractDataAndFlags(method, dataArray, flagArray);
}
const inputError = logErrorFromInput(path, method, flags, dataArray);
if (inputError) { if (inputError) {
this.logAndOutput(command, inputError); this.logAndOutput(command, inputError);
return; return;
} }
try { try {
const resp = yield service[method].call(service, path, data, flags.wrapTTL); const resp = yield service[camelize(method)].call(service, path, data, flags);
this.logAndOutput(command, logFromResponse(resp, path, method, flags)); this.logAndOutput(command, logFromResponse(resp, path, method, flags));
} catch (error) { } catch (error) {
if (error instanceof ControlGroupError) { if (error instanceof ControlGroupError) {

View File

@ -39,7 +39,7 @@
</:footer> </:footer>
</Hds::SideNav> </Hds::SideNav>
</Frame.Sidebar> </Frame.Sidebar>
<Frame.Main id="app-main-content"> <Frame.Main id="app-main-content" class={{if this.console.isOpen "main--console-open"}}>
{{! outlet for app content }} {{! outlet for app content }}
<div id="modal-wormhole"></div> <div id="modal-wormhole"></div>
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} /> <LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />

View File

@ -4,20 +4,57 @@
*/ */
import keys from 'vault/lib/keycodes'; import keys from 'vault/lib/keycodes';
import argTokenizer from './arg-tokenizer'; import AdapterError from '@ember-data/adapter/error';
import { parse } from 'shell-quote'; import { parse } from 'shell-quote';
const supportedCommands = ['read', 'write', 'list', 'delete']; import argTokenizer from './arg-tokenizer';
import { StringMap } from 'vault/vault/app-types';
// Add new commands to `log-help` component for visibility
const supportedCommands = ['read', 'write', 'list', 'delete', 'kv-get'];
const uiCommands = ['api', 'clearall', 'clear', 'fullscreen', 'refresh']; const uiCommands = ['api', 'clearall', 'clear', 'fullscreen', 'refresh'];
export function extractDataAndFlags(method, data, flags) { interface DataObj {
return data.concat(flags).reduce( [key: string]: string | string[];
(accumulator, val) => { }
// will be "key=value" or "-flag=value" or "foo=bar=baz"
export function extractDataFromStrings(dataArray: string[]): DataObj {
if (!dataArray) return {};
return dataArray.reduce((accumulator: DataObj, val: string) => {
// will be "key=value" or "foo=bar=baz"
// split on the first = // split on the first =
// default to value of empty string // default to value of empty string
const [item, value = ''] = val.split(/=(.+)?/); const [item = '', value = ''] = val.split(/=(.+)?/);
if (item.startsWith('-')) { if (!item) return accumulator;
// if it exists in data already, then we have multiple
// foo=bar in the list and need to make it an array
const existingValue = accumulator[item];
if (existingValue) {
accumulator[item] = Array.isArray(existingValue) ? [...existingValue, value] : [existingValue, value];
return accumulator;
}
accumulator[item] = value;
return accumulator;
}, {});
}
interface Flags {
field?: string;
format?: string;
force?: boolean;
wrapTTL?: boolean;
[key: string]: string | boolean | undefined;
}
export function extractFlagsFromStrings(flagArray: string[], method: string): Flags {
if (!flagArray) return {};
return flagArray.reduce((accumulator: Flags, val: string) => {
// val will be "-flag=value" or "--force"
// split on the first =
// default to value or true
const [item, value] = val.split(/=(.+)?/);
if (!item) return accumulator;
let flagName = item.replace(/^-/, ''); let flagName = item.replace(/^-/, '');
if (flagName === 'wrap-ttl') { if (flagName === 'wrap-ttl') {
flagName = 'wrapTTL'; flagName = 'wrapTTL';
@ -26,44 +63,48 @@ export function extractDataAndFlags(method, data, flags) {
flagName = 'force'; flagName = 'force';
} }
} }
accumulator.flags[flagName] = value || true; accumulator[flagName] = value || true;
return accumulator; return accumulator;
} }, {});
// if it exists in data already, then we have multiple
// foo=bar in the list and need to make it an array
if (accumulator.data[item]) {
accumulator.data[item] = [].concat(accumulator.data[item], value);
return accumulator;
}
accumulator.data[item] = value;
return accumulator;
},
{ data: {}, flags: {} }
);
} }
export function executeUICommand(command, logAndOutput, commandFns) { interface CommandFns {
[key: string]: CallableFunction;
}
export function executeUICommand(
command: string,
logAndOutput: CallableFunction,
commandFns: CommandFns
): boolean {
const cmd = command.startsWith('api') ? 'api' : command; const cmd = command.startsWith('api') ? 'api' : command;
const isUICommand = uiCommands.includes(cmd); const isUICommand = uiCommands.includes(cmd);
if (isUICommand) { if (isUICommand) {
logAndOutput(command); logAndOutput(command);
} }
if (typeof commandFns[cmd] === 'function') { const execCommand = commandFns[cmd];
commandFns[cmd](); if (execCommand && typeof execCommand === 'function') {
execCommand();
} }
return isUICommand; return isUICommand;
} }
export function parseCommand(command, shouldThrow) { interface ParsedCommand {
const args = argTokenizer(parse(command)); method: string;
path: string;
flagArray: string[];
dataArray: string[];
}
export function parseCommand(command: string): ParsedCommand {
const args: string[] = argTokenizer(parse(command));
if (args[0] === 'vault') { if (args[0] === 'vault') {
args.shift(); args.shift();
} }
const [method, ...rest] = args; const [method = '', ...rest] = args;
let path; let path = '';
const flags = []; const flags: string[] = [];
const data = []; const data: string[] = [];
rest.forEach((arg) => { rest.forEach((arg) => {
if (arg.startsWith('-')) { if (arg.startsWith('-')) {
@ -86,24 +127,28 @@ export function parseCommand(command, shouldThrow) {
}); });
if (!supportedCommands.includes(method)) { if (!supportedCommands.includes(method)) {
if (shouldThrow) {
throw new Error('invalid command'); throw new Error('invalid command');
} }
return false; return { method, flagArray: flags, path, dataArray: data };
}
return [method, flags, path, data];
} }
export function logFromResponse(response, path, method, flags) { interface LogResponse {
auth?: StringMap;
data?: StringMap;
wrap_info?: StringMap;
[key: string]: unknown;
}
export function logFromResponse(response: LogResponse, path: string, method: string, flags: Flags) {
const { format, field } = flags; const { format, field } = flags;
let secret = response && (response.auth || response.data || response.wrap_info); const respData: StringMap | undefined = response && (response.auth || response.data || response.wrap_info);
if (!secret) { const secret: StringMap | LogResponse = respData || response;
if (!respData) {
if (method === 'write') { if (method === 'write') {
return { type: 'success', content: `Success! Data written to: ${path}` }; return { type: 'success', content: `Success! Data written to: ${path}` };
} else if (method === 'delete') { } else if (method === 'delete') {
return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` }; return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` };
} else {
secret = response;
} }
} }
@ -143,11 +188,17 @@ export function logFromResponse(response, path, method, flags) {
return { type: 'object', content: secret }; return { type: 'object', content: secret };
} }
export function logFromError(error, vaultPath, method) { interface CustomError extends AdapterError {
httpStatus: number;
path: string;
errors: string[];
}
export function logFromError(error: CustomError, vaultPath: string, method: string) {
let content; let content;
const { httpStatus, path } = error; const { httpStatus, path } = error;
const verbClause = { const verbClause = {
read: 'reading from', read: 'reading from',
'kv-get': 'reading secret',
write: 'writing to', write: 'writing to',
list: 'listing', list: 'listing',
delete: 'deleting at', delete: 'deleting at',
@ -162,7 +213,11 @@ export function logFromError(error, vaultPath, method) {
return { type: 'error', content }; return { type: 'error', content };
} }
export function shiftCommandIndex(keyCode, history, index) { interface CommandLog {
type: string;
content?: string;
}
export function shiftCommandIndex(keyCode: number, history: CommandLog[], index: number) {
let newInputValue; let newInputValue;
const commandHistoryLength = history.length; const commandHistoryLength = history.length;
@ -186,17 +241,18 @@ export function shiftCommandIndex(keyCode, history, index) {
} }
if (newInputValue !== '') { if (newInputValue !== '') {
newInputValue = history.objectAt(index).content; newInputValue = history.objectAt(index)?.content;
} }
return [index, newInputValue]; return [index, newInputValue];
} }
export function logErrorFromInput(path, method, flags, dataArray) { export function formattedErrorFromInput(path: string, method: string, flags: Flags, dataArray: string[]) {
if (path === undefined) { if (path === undefined) {
return { type: 'error', content: 'A path is required to make a request.' }; return { type: 'error', content: 'A path is required to make a request.' };
} }
if (method === 'write' && !flags.force && dataArray.length === 0) { if (method === 'write' && !flags.force && dataArray.length === 0) {
return { type: 'error', content: 'Must supply data or use -force' }; return { type: 'error', content: 'Must supply data or use -force' };
} }
return;
} }

View File

@ -84,11 +84,22 @@ export default Service.extend({
}); });
}, },
read(path, data, wrapTTL) { kvGet(path, data, flags = {}) {
const { wrapTTL, metadata } = flags;
// Split on first / to find backend and secret path
const pathSegment = metadata ? 'metadata' : 'data';
const [backend, secretPath] = path.split(/\/(.+)?/);
const kvPath = `${backend}/${pathSegment}/${secretPath}`;
return this.ajax('read', sanitizePath(kvPath), { wrapTTL });
},
read(path, data, flags) {
const wrapTTL = flags?.wrapTTL;
return this.ajax('read', sanitizePath(path), { wrapTTL }); return this.ajax('read', sanitizePath(path), { wrapTTL });
}, },
write(path, data, wrapTTL) { write(path, data, flags) {
const wrapTTL = flags?.wrapTTL;
return this.ajax('write', sanitizePath(path), { data, wrapTTL }); return this.ajax('write', sanitizePath(path), { data, wrapTTL });
}, },
@ -96,7 +107,8 @@ export default Service.extend({
return this.ajax('delete', sanitizePath(path)); return this.ajax('delete', sanitizePath(path));
}, },
list(path, data, wrapTTL) { list(path, data, flags) {
const wrapTTL = flags?.wrapTTL;
const listPath = ensureTrailingSlash(sanitizePath(path)); const listPath = ensureTrailingSlash(sanitizePath(path));
return this.ajax('list', listPath, { return this.ajax('list', listPath, {
data: { data: {

View File

@ -125,6 +125,10 @@ $console-close-height: 35px;
min-height: 400px; min-height: 400px;
} }
.main--console-open {
padding-bottom: 400px;
}
.panel-open .console-ui-panel.fullscreen { .panel-open .console-ui-panel.fullscreen {
bottom: 0; bottom: 0;
right: 0; right: 0;

View File

@ -6,6 +6,7 @@
Commands: Commands:
read Read data and retrieves secrets read Read data and retrieves secrets
kv-get Read data for kv v2 secret engines. Use -metadata flag to read metadata
write Write data, configuration, and secrets write Write data, configuration, and secrets
delete Delete secrets and configuration delete Delete secrets and configuration
list List data or secrets list List data or secrets

View File

@ -5,10 +5,18 @@
</div> </div>
<div class="console-ui-panel-content"> <div class="console-ui-panel-content">
<div class="content has-bottom-margin-l"> <div class="content has-bottom-margin-l">
<p class="has-text-grey is-font-mono"> <p class="has-text-grey is-font-mono has-bottom-margin-s">
The Vault Browser CLI provides an easy way to execute the most common CLI commands, such as write, read, delete, and The Vault Browser CLI provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list.
list. It does not include kv v2 write or put commands. For guidance, type `help`.
</p> </p>
<p class="has-text-grey is-font-mono has-bottom-margin-s">Examples:</p>
<p class="has-text-grey is-font-mono">→ Write secrets to kv v1: write &lt;mount&gt;/my-secret foo=bar</p>
<p class="has-text-grey is-font-mono">→ List kv v1 secret keys: list &lt;mount&gt;/</p>
<p class="has-text-grey is-font-mono">→ Read a kv v1 secret: read &lt;mount&gt;/my-secret</p>
<p class="has-text-grey is-font-mono">→ Mount a kv v2 secret engine: write sys/mounts/&lt;mount&gt; type=kv
options=version=2</p>
<p class="has-text-grey is-font-mono">→ Read a kv v2 secret: kv-get &lt;mount&gt;/secret-path</p>
<p class="has-text-grey is-font-mono">→ Read a kv v2 secret's metadata: kv-get &lt;mount&gt;/secret-path -metadata</p>
</div> </div>
<Console::OutputLog @outputLog={{this.cliLog}} /> <Console::OutputLog @outputLog={{this.cliLog}} />
<Console::CommandInput <Console::CommandInput

View File

@ -10,8 +10,11 @@
<ToolbarFilters> <ToolbarFilters>
<div class="field is-marginless"> <div class="field is-marginless">
<p class="control has-icons-left"> <p class="control has-icons-left">
<label for="swagger-result-filter" class="sr-only">Filter operations by path</label>
<input <input
oninput={{queue (action "updateFilter") (action "proxyEvent")}} id="swagger-result-filter"
{{on "input" (action "proxyEvent")}}
{{on "change" (action "updateFilter")}}
value={{@initialFilter}} value={{@initialFilter}}
disabled={{this.swaggerLoading}} disabled={{this.swaggerLoading}}
class="filter input" class="filter input"

View File

@ -92,6 +92,7 @@
"@types/ember__utils": "^4.0.2", "@types/ember__utils": "^4.0.2",
"@types/qunit": "^2.19.3", "@types/qunit": "^2.19.3",
"@types/rsvp": "^4.0.4", "@types/rsvp": "^4.0.4",
"@types/shell-quote": "^1.7.1",
"@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0", "@typescript-eslint/parser": "^5.19.0",
"asn1js": "^2.2.0", "asn1js": "^2.2.0",

View File

@ -6,10 +6,11 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { import {
parseCommand, parseCommand,
extractDataAndFlags,
logFromResponse, logFromResponse,
logFromError, logFromError,
logErrorFromInput, formattedErrorFromInput,
extractFlagsFromStrings,
extractDataFromStrings,
} from 'vault/lib/console-helpers'; } from 'vault/lib/console-helpers';
module('Unit | Lib | console helpers', function () { module('Unit | Lib | console helpers', function () {
@ -20,16 +21,16 @@ module('Unit | Lib | console helpers', function () {
access_key=AKIAJWVN5Z4FOFT7NLNA \ access_key=AKIAJWVN5Z4FOFT7NLNA \
secret_key=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 \ secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i \
region=us-east-1`, region=us-east-1`,
expected: [ expected: {
'write', method: 'write',
[], flagArray: [],
'aws/config/root', path: 'aws/config/root',
[ dataArray: [
'access_key=AKIAJWVN5Z4FOFT7NLNA', 'access_key=AKIAJWVN5Z4FOFT7NLNA',
'secret_key=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', 'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
'region=us-east-1', 'region=us-east-1',
], ],
], },
}, },
{ {
name: 'write with space in a value', name: 'write with space in a value',
@ -43,11 +44,11 @@ module('Unit | Lib | console helpers', function () {
insecure_tls=true \ insecure_tls=true \
starttls=false starttls=false
`, `,
expected: [ expected: {
'write', method: 'write',
[], flagArray: [],
'auth/ldap/config', path: 'auth/ldap/config',
[ dataArray: [
'url=ldap://ldap.example.com:3268', 'url=ldap://ldap.example.com:3268',
'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com', 'binddn=CN=ServiceViewDev,OU=Service Accounts,DC=example,DC=com',
'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx', 'bindpass=xxxxxxxxxxxxxxxxxxxxxxxxxx',
@ -56,7 +57,7 @@ module('Unit | Lib | console helpers', function () {
'insecure_tls=true', 'insecure_tls=true',
'starttls=false', 'starttls=false',
], ],
], },
}, },
{ {
name: 'write with double quotes', name: 'write with double quotes',
@ -64,7 +65,7 @@ module('Unit | Lib | console helpers', function () {
auth/token/create \ auth/token/create \
policies="foo" policies="foo"
`, `,
expected: ['write', [], 'auth/token/create', ['policies=foo']], expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] },
}, },
{ {
name: 'write with single quotes', name: 'write with single quotes',
@ -72,7 +73,7 @@ module('Unit | Lib | console helpers', function () {
auth/token/create \ auth/token/create \
policies='foo' policies='foo'
`, `,
expected: ['write', [], 'auth/token/create', ['policies=foo']], expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ['policies=foo'] },
}, },
{ {
name: 'write with unmatched quotes', name: 'write with unmatched quotes',
@ -80,30 +81,35 @@ module('Unit | Lib | console helpers', function () {
auth/token/create \ auth/token/create \
policies="'foo" policies="'foo"
`, `,
expected: ['write', [], 'auth/token/create', ["policies='foo"]], expected: { method: 'write', flagArray: [], path: 'auth/token/create', dataArray: ["policies='foo"] },
}, },
{ {
name: 'write with shell characters', name: 'write with shell characters',
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
command: `vault write database/roles/api-prod db_name=apiprod creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" default_ttl=1h max_ttl=24h command: `vault write database/roles/api-prod db_name=apiprod creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" default_ttl=1h max_ttl=24h
`, `,
expected: [ expected: {
'write', method: 'write',
[], flagArray: [],
'database/roles/api-prod', path: 'database/roles/api-prod',
[ dataArray: [
'db_name=apiprod', 'db_name=apiprod',
`creation_statements=CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};`, `creation_statements=CREATE ROLE {{name}} WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO {{name}};`,
'default_ttl=1h', 'default_ttl=1h',
'max_ttl=24h', 'max_ttl=24h',
], ],
], },
}, },
{ {
name: 'read with field', name: 'read with field',
command: `vault read -field=access_key aws/creds/my-role`, command: `vault read -field=access_key aws/creds/my-role`,
expected: ['read', ['-field=access_key'], 'aws/creds/my-role', []], expected: {
method: 'read',
flagArray: ['-field=access_key'],
path: 'aws/creds/my-role',
dataArray: [],
},
}, },
]; ];
@ -115,16 +121,14 @@ module('Unit | Lib | console helpers', function () {
}); });
test('#parseCommand: invalid commands', function (assert) { test('#parseCommand: invalid commands', function (assert) {
assert.expect(1);
const command = 'vault kv get foo'; const command = 'vault kv get foo';
const result = parseCommand(command);
assert.false(result, 'parseCommand returns false by default');
assert.throws( assert.throws(
() => { () => {
parseCommand(command, true); parseCommand(command);
}, },
/invalid command/, /invalid command/,
'throws on invalid command when `shouldThrow` is true' 'throws on invalid command'
); );
}); });
@ -132,14 +136,12 @@ module('Unit | Lib | console helpers', function () {
{ {
method: 'read', method: 'read',
name: 'data fields', name: 'data fields',
input: [ dataInput: [
[
'access_key=AKIAJWVN5Z4FOFT7NLNA', 'access_key=AKIAJWVN5Z4FOFT7NLNA',
'secret_key=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', 'secret_key=R4nm063hgMVo4BTT5xOs5nHLeLXA6lar7ZJ3Nt0i',
'region=us-east-1', 'region=us-east-1',
], ],
[], flagInput: [],
],
expected: { expected: {
data: { data: {
access_key: 'AKIAJWVN5Z4FOFT7NLNA', access_key: 'AKIAJWVN5Z4FOFT7NLNA',
@ -152,7 +154,8 @@ module('Unit | Lib | console helpers', function () {
{ {
method: 'read', method: 'read',
name: 'repeated data and a flag', name: 'repeated data and a flag',
input: [['allowed_domains=example.com', 'allowed_domains=foo.example.com'], ['-wrap-ttl=2h']], dataInput: ['allowed_domains=example.com', 'allowed_domains=foo.example.com'],
flagInput: ['-wrap-ttl=2h'],
expected: { expected: {
data: { data: {
allowed_domains: ['example.com', 'foo.example.com'], allowed_domains: ['example.com', 'foo.example.com'],
@ -162,10 +165,27 @@ module('Unit | Lib | console helpers', function () {
}, },
}, },
}, },
{
method: 'read',
name: 'triple data',
dataInput: [
'allowed_domains=example.com',
'allowed_domains=foo.example.com',
'allowed_domains=dev.example.com',
],
flagInput: [],
expected: {
data: {
allowed_domains: ['example.com', 'foo.example.com', 'dev.example.com'],
},
flags: {},
},
},
{ {
method: 'read', method: 'read',
name: 'data with more than one equals sign', name: 'data with more than one equals sign',
input: [['foo=bar=baz', 'foo=baz=bop', 'some=value=val'], []], dataInput: ['foo=bar=baz', 'foo=baz=bop', 'some=value=val'],
flagInput: [],
expected: { expected: {
data: { data: {
foo: ['bar=baz', 'baz=bop'], foo: ['bar=baz', 'baz=bop'],
@ -177,7 +197,8 @@ module('Unit | Lib | console helpers', function () {
{ {
method: 'read', method: 'read',
name: 'data with empty values', name: 'data with empty values',
input: [[`foo=`, 'some=thing'], []], dataInput: [`foo=`, 'some=thing'],
flagInput: [],
expected: { expected: {
data: { data: {
foo: '', foo: '',
@ -189,7 +210,8 @@ module('Unit | Lib | console helpers', function () {
{ {
method: 'write', method: 'write',
name: 'write with force flag', name: 'write with force flag',
input: [[], ['-force']], dataInput: [],
flagInput: ['-force'],
expected: { expected: {
data: {}, data: {},
flags: { flags: {
@ -200,7 +222,8 @@ module('Unit | Lib | console helpers', function () {
{ {
method: 'write', method: 'write',
name: 'write with force short flag', name: 'write with force short flag',
input: [[], ['-f']], dataInput: [],
flagInput: ['-f'],
expected: { expected: {
data: {}, data: {},
flags: { flags: {
@ -211,7 +234,8 @@ module('Unit | Lib | console helpers', function () {
{ {
method: 'write', method: 'write',
name: 'write with GNU style force flag', name: 'write with GNU style force flag',
input: [[], ['--force']], dataInput: [],
flagInput: ['--force'],
expected: { expected: {
data: {}, data: {},
flags: { flags: {
@ -222,9 +246,12 @@ module('Unit | Lib | console helpers', function () {
]; ];
testExtractCases.forEach(function (testCase) { testExtractCases.forEach(function (testCase) {
test(`#extractDataAndFlags: ${testCase.name}`, function (assert) { test(`#extractDataFromStrings: ${testCase.name}`, function (assert) {
const { data, flags } = extractDataAndFlags(testCase.method, ...testCase.input); const data = extractDataFromStrings(testCase.dataInput);
assert.deepEqual(data, testCase.expected.data, 'has expected data'); assert.deepEqual(data, testCase.expected.data, 'has expected data');
});
test(`#extractFlagsFromStrings: ${testCase.name}`, function (assert) {
const flags = extractFlagsFromStrings(testCase.flagInput, testCase.method);
assert.deepEqual(flags, testCase.expected.flags, 'has expected flags'); assert.deepEqual(flags, testCase.expected.flags, 'has expected flags');
}); });
}); });
@ -469,8 +496,8 @@ module('Unit | Lib | console helpers', function () {
]; ];
testCommandCases.forEach(function (testCase) { testCommandCases.forEach(function (testCase) {
test(`#logErrorFromInput: ${testCase.name}`, function (assert) { test(`#formattedErrorFromInput: ${testCase.name}`, function (assert) {
const data = logErrorFromInput(...testCase.args); const data = formattedErrorFromInput(...testCase.args);
assert.deepEqual( assert.deepEqual(
data, data,

View File

@ -38,7 +38,7 @@ module('Unit | Service | console', function (hooks) {
{ {
method: 'read', method: 'read',
args: ['/secrets/foo/bar', {}, '30m'], args: ['/secrets/foo/bar', {}, { wrapTTL: '30m' }],
expectedURL: 'secrets/foo/bar', expectedURL: 'secrets/foo/bar',
expectedVerb: 'GET', expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: '30m' }, expectedOptions: { data: undefined, wrapTTL: '30m' },
@ -65,7 +65,7 @@ module('Unit | Service | console', function (hooks) {
{ {
method: 'list', method: 'list',
args: ['secret/mounts', {}, '1h'], args: ['secret/mounts', {}, { wrapTTL: '1h' }],
expectedURL: 'secret/mounts/', expectedURL: 'secret/mounts/',
expectedVerb: 'GET', expectedVerb: 'GET',
expectedOptions: { data: { list: true }, wrapTTL: '1h' }, expectedOptions: { data: { list: true }, wrapTTL: '1h' },
@ -102,4 +102,58 @@ module('Unit | Service | console', function (hooks) {
assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`); assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`);
}); });
}); });
const kvTestCases = [
{
method: 'kvGet',
args: ['kv/foo'],
expectedURL: 'kv/data/foo',
expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: undefined },
},
{
method: 'kvGet',
args: ['kv/foo', {}, { metadata: true }],
expectedURL: 'kv/metadata/foo',
expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: undefined },
},
{
method: 'kvGet',
args: ['kv/foo', {}, { wrapTTL: '10m' }],
expectedURL: 'kv/data/foo',
expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: '10m' },
},
{
method: 'kvGet',
args: ['kv/foo', {}, { metadata: true, wrapTTL: '10m' }],
expectedURL: 'kv/metadata/foo',
expectedVerb: 'GET',
expectedOptions: { data: undefined, wrapTTL: '10m' },
},
];
test('it reads kv secret and metadata', function (assert) {
assert.expect(12);
const ajax = sinon.stub();
const uiConsole = this.owner.factoryFor('service:console').create({
adapter() {
return {
buildURL(url) {
return url;
},
ajax,
};
},
});
kvTestCases.forEach((testCase) => {
uiConsole[testCase.method](...testCase.args);
const [url, verb, options] = ajax.lastCall.args;
assert.strictEqual(url, testCase.expectedURL, `${testCase.method}: uses correct url`);
assert.strictEqual(verb, testCase.expectedVerb, `${testCase.method}: uses the correct verb`);
assert.deepEqual(options, testCase.expectedOptions, `${testCase.method}: uses the correct options`);
});
});
}); });

View File

@ -5648,6 +5648,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/shell-quote@npm:^1.7.1":
version: 1.7.1
resolution: "@types/shell-quote@npm:1.7.1"
checksum: 51e58326b8c6dcb72846b94cebe3dc4c84f3514469a8e52bd29c52c601e784b427b851d7477acbeef47bfcccf25d2a5768684d27e7fc95fdd003393c1bbb7bc3
languageName: node
linkType: hard
"@types/symlink-or-copy@npm:^1.2.0": "@types/symlink-or-copy@npm:^1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "@types/symlink-or-copy@npm:1.2.0" resolution: "@types/symlink-or-copy@npm:1.2.0"
@ -24224,6 +24231,7 @@ __metadata:
"@types/ember__utils": ^4.0.2 "@types/ember__utils": ^4.0.2
"@types/qunit": ^2.19.3 "@types/qunit": ^2.19.3
"@types/rsvp": ^4.0.4 "@types/rsvp": ^4.0.4
"@types/shell-quote": ^1.7.1
"@typescript-eslint/eslint-plugin": ^5.19.0 "@typescript-eslint/eslint-plugin": ^5.19.0
"@typescript-eslint/parser": ^5.19.0 "@typescript-eslint/parser": ^5.19.0
asn1js: ^2.2.0 asn1js: ^2.2.0