Skip to content

Commit

Permalink
UI/console update (hashicorp#20590)
Browse files Browse the repository at this point in the history
  • Loading branch information
hashishaw authored May 17, 2023
1 parent d234111 commit 7c66970
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 133 deletions.
3 changes: 3 additions & 0 deletions changelog/20590.txt
Original file line number Diff line number Diff line change
@@ -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
```
8 changes: 0 additions & 8 deletions ui/app/components/console/log-help.js

This file was deleted.

28 changes: 13 additions & 15 deletions ui/app/components/console/ui-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { alias, or } from '@ember/object/computed';
import Component from '@ember/component';
import { getOwner } from '@ember/application';
import { schedule } from '@ember/runloop';
import { camelize } from '@ember/string';
import { task } from 'ember-concurrency';
import ControlGroupError from 'vault/lib/control-group-error';
import {
parseCommand,
extractDataAndFlags,
logFromResponse,
logFromError,
logErrorFromInput,
formattedErrorFromInput,
executeUICommand,
extractFlagsFromStrings,
extractDataFromStrings,
} from 'vault/lib/console-helpers';

export default Component.extend({
Expand Down Expand Up @@ -64,29 +66,25 @@ export default Component.extend({

// parse to verify it's valid
try {
serviceArgs = parseCommand(command, shouldThrow);
serviceArgs = parseCommand(command);
} catch (e) {
this.logAndOutput(command, { type: 'help' });
return;
}
// we have a invalid command but don't want to throw
if (serviceArgs === false) {
if (shouldThrow) {
this.logAndOutput(command, { type: 'help' });
}
return;
}

const [method, flagArray, path, dataArray] = serviceArgs;

if (dataArray || flagArray) {
var { data, flags } = extractDataAndFlags(method, dataArray, flagArray);
}
const { method, flagArray, path, dataArray } = serviceArgs;
const flags = extractFlagsFromStrings(flagArray, method);
const data = extractDataFromStrings(dataArray);

const inputError = logErrorFromInput(path, method, flags, dataArray);
const inputError = formattedErrorFromInput(path, method, flags, dataArray);
if (inputError) {
this.logAndOutput(command, inputError);
return;
}
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));
} catch (error) {
if (error instanceof ControlGroupError) {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/components/sidebar/frame.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</:footer>
</Hds::SideNav>
</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 }}
<div id="modal-wormhole"></div>
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
Expand Down
164 changes: 110 additions & 54 deletions ui/app/lib/console-helpers.js → ui/app/lib/console-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,107 @@
*/

import keys from 'vault/lib/keycodes';
import argTokenizer from './arg-tokenizer';
import AdapterError from '@ember-data/adapter/error';
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'];

export function extractDataAndFlags(method, data, flags) {
return data.concat(flags).reduce(
(accumulator, val) => {
// will be "key=value" or "-flag=value" or "foo=bar=baz"
// split on the first =
// default to value of empty string
const [item, value = ''] = val.split(/=(.+)?/);
if (item.startsWith('-')) {
let flagName = item.replace(/^-/, '');
if (flagName === 'wrap-ttl') {
flagName = 'wrapTTL';
} else if (method === 'write') {
if (flagName === 'f' || flagName === '-force') {
flagName = 'force';
}
}
accumulator.flags[flagName] = value || true;
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;
interface DataObj {
[key: string]: string | string[];
}

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 =
// default to value of empty string
const [item = '', value = ''] = val.split(/=(.+)?/);
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;
},
{ data: {}, flags: {} }
);
}
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;

export function executeUICommand(command, logAndOutput, commandFns) {
let flagName = item.replace(/^-/, '');
if (flagName === 'wrap-ttl') {
flagName = 'wrapTTL';
} else if (method === 'write') {
if (flagName === 'f' || flagName === '-force') {
flagName = 'force';
}
}
accumulator[flagName] = value || true;
return accumulator;
}, {});
}

interface CommandFns {
[key: string]: CallableFunction;
}

export function executeUICommand(
command: string,
logAndOutput: CallableFunction,
commandFns: CommandFns
): boolean {
const cmd = command.startsWith('api') ? 'api' : command;
const isUICommand = uiCommands.includes(cmd);
if (isUICommand) {
logAndOutput(command);
}
if (typeof commandFns[cmd] === 'function') {
commandFns[cmd]();
const execCommand = commandFns[cmd];
if (execCommand && typeof execCommand === 'function') {
execCommand();
}
return isUICommand;
}

export function parseCommand(command, shouldThrow) {
const args = argTokenizer(parse(command));
interface ParsedCommand {
method: string;
path: string;
flagArray: string[];
dataArray: string[];
}
export function parseCommand(command: string): ParsedCommand {
const args: string[] = argTokenizer(parse(command));
if (args[0] === 'vault') {
args.shift();
}

const [method, ...rest] = args;
let path;
const flags = [];
const data = [];
const [method = '', ...rest] = args;
let path = '';
const flags: string[] = [];
const data: string[] = [];

rest.forEach((arg) => {
if (arg.startsWith('-')) {
Expand All @@ -86,24 +127,28 @@ export function parseCommand(command, shouldThrow) {
});

if (!supportedCommands.includes(method)) {
if (shouldThrow) {
throw new Error('invalid command');
}
return false;
throw new Error('invalid command');
}
return [method, flags, path, data];
return { method, flagArray: flags, path, dataArray: 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;
let secret = response && (response.auth || response.data || response.wrap_info);
if (!secret) {
const respData: StringMap | undefined = response && (response.auth || response.data || response.wrap_info);
const secret: StringMap | LogResponse = respData || response;

if (!respData) {
if (method === 'write') {
return { type: 'success', content: `Success! Data written to: ${path}` };
} else if (method === 'delete') {
return { type: 'success', content: `Success! Data deleted (if it existed) at: ${path}` };
} else {
secret = response;
}
}

Expand Down Expand Up @@ -143,11 +188,17 @@ export function logFromResponse(response, path, method, flags) {
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;
const { httpStatus, path } = error;
const verbClause = {
read: 'reading from',
'kv-get': 'reading secret',
write: 'writing to',
list: 'listing',
delete: 'deleting at',
Expand All @@ -162,7 +213,11 @@ export function logFromError(error, vaultPath, method) {
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;
const commandHistoryLength = history.length;

Expand All @@ -186,17 +241,18 @@ export function shiftCommandIndex(keyCode, history, index) {
}

if (newInputValue !== '') {
newInputValue = history.objectAt(index).content;
newInputValue = history.objectAt(index)?.content;
}

return [index, newInputValue];
}

export function logErrorFromInput(path, method, flags, dataArray) {
export function formattedErrorFromInput(path: string, method: string, flags: Flags, dataArray: string[]) {
if (path === undefined) {
return { type: 'error', content: 'A path is required to make a request.' };
}
if (method === 'write' && !flags.force && dataArray.length === 0) {
return { type: 'error', content: 'Must supply data or use -force' };
}
return;
}
18 changes: 15 additions & 3 deletions ui/app/services/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,31 @@ 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 });
},

write(path, data, wrapTTL) {
write(path, data, flags) {
const wrapTTL = flags?.wrapTTL;
return this.ajax('write', sanitizePath(path), { data, wrapTTL });
},

delete(path) {
return this.ajax('delete', sanitizePath(path));
},

list(path, data, wrapTTL) {
list(path, data, flags) {
const wrapTTL = flags?.wrapTTL;
const listPath = ensureTrailingSlash(sanitizePath(path));
return this.ajax('list', listPath, {
data: {
Expand Down
4 changes: 4 additions & 0 deletions ui/app/styles/components/console-ui-panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ $console-close-height: 35px;
min-height: 400px;
}

.main--console-open {
padding-bottom: 400px;
}

.panel-open .console-ui-panel.fullscreen {
bottom: 0;
right: 0;
Expand Down
1 change: 1 addition & 0 deletions ui/app/templates/components/console/log-help.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

Commands:
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
delete Delete secrets and configuration
list List data or secrets
Expand Down
14 changes: 11 additions & 3 deletions ui/app/templates/components/console/ui-panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
</div>
<div class="console-ui-panel-content">
<div class="content has-bottom-margin-l">
<p class="has-text-grey is-font-mono">
The Vault Browser CLI provides an easy way to execute the most common CLI commands, such as write, read, delete, and
list.
<p class="has-text-grey is-font-mono has-bottom-margin-s">
The Vault Browser CLI provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list.
It does not include kv v2 write or put commands. For guidance, type `help`.
</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>
<Console::OutputLog @outputLog={{this.cliLog}} />
<Console::CommandInput
Expand Down
Loading

0 comments on commit 7c66970

Please sign in to comment.