convert commands key, keys, key add, key rm to oclif.

Also:
 - Display keys with `name` instead of `title`.
 - Check for empty key before calling API.

Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
Scott Lowe 2020-04-17 15:19:33 +02:00
parent 7c9a23451b
commit be82bcfa63
16 changed files with 438 additions and 161 deletions

View File

@ -68,7 +68,12 @@ const capitanoDoc = {
},
{
title: 'Keys',
files: ['build/actions/keys.js'],
files: [
'build/actions-oclif/keys.js',
'build/actions-oclif/key/index.js',
'build/actions-oclif/key/add.js',
'build/actions-oclif/key/rm.js',
],
},
{
title: 'Logs',

View File

@ -190,8 +190,8 @@ If you come across any problems or would like to get in touch:
- [keys](#keys)
- [key &#60;id&#62;](#key-id)
- [key rm &#60;id&#62;](#key-rm-id)
- [key add &#60;name&#62; [path]](#key-add-name-path)
- [key rm &#60;id&#62;](#key-rm-id)
- Logs
@ -1063,41 +1063,33 @@ output version information in JSON format for programmatic use
## keys
Use this command to list all your SSH keys.
List all SSH keys registered in balenaCloud for the logged in user.
Examples:
$ balena keys
### Options
## key &#60;id&#62;
Use this command to show information about a single SSH key.
Display a single SSH key registered in balenaCloud for the logged in user.
Examples:
$ balena key 17
## key rm &#60;id&#62;
### Arguments
Use this command to remove a SSH key from balena.
#### ID
Notice this command asks for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Examples:
$ balena key rm 17
$ balena key rm 17 --yes
balenaCloud ID for the SSH key
### Options
#### --yes, -y
confirm non interactively
## key add &#60;name&#62; [path]
Use this command to associate a new SSH key with your account.
Register an SSH in balenaCloud for the logged in user.
If `path` is omitted, the command will attempt
to read the SSH key from stdin.
@ -1107,6 +1099,41 @@ Examples:
$ balena key add Main ~/.ssh/id_rsa.pub
$ cat ~/.ssh/id_rsa.pub | balena key add Main
### Arguments
#### NAME
the SSH key name
#### PATH
the path to the public key file
### Options
## key rm &#60;id&#62;
Remove a single SSH key registered in balenaCloud for the logged in user.
The --yes option may be used to avoid interactive confirmation.
Examples:
$ balena key rm 17
$ balena key rm 17 --yes
### Arguments
#### ID
balenaCloud ID for the SSH key
### Options
#### -y, --yes
answer "yes" to all questions (non interactive use)
# Logs
## logs &#60;uuidOrDevice&#62;

View File

@ -22,6 +22,7 @@ import * as cf from '../../utils/common-flags';
import * as ec from '../../utils/env-common';
import { getBalenaSdk } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
@ -60,7 +61,7 @@ export default class EnvRenameCmd extends Command {
name: 'id',
required: true,
description: "variable's numeric database ID",
parse: input => ec.parseDbId(input),
parse: input => parseAsInteger(input, 'id'),
},
{
name: 'value',

View File

@ -22,6 +22,7 @@ import Command from '../../command';
import * as ec from '../../utils/env-common';
import { getBalenaSdk } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
@ -63,7 +64,7 @@ export default class EnvRmCmd extends Command {
name: 'id',
required: true,
description: "variable's numeric database ID",
parse: input => ec.parseDbId(input),
parse: input => parseAsInteger(input, 'id'),
},
];

View File

@ -0,0 +1,87 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* 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.
*/
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
path: string;
}
export default class KeyAddCmd extends Command {
public static description = stripIndent`
Add an SSH key to balenaCloud.
Register an SSH in balenaCloud for the logged in user.
If \`path\` is omitted, the command will attempt
to read the SSH key from stdin.
`;
public static examples = [
'$ balena key add Main ~/.ssh/id_rsa.pub',
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
];
public static args = [
{
name: 'name',
description: 'the SSH key name',
required: true,
},
{
name: `path`,
description: `the path to the public key file`,
},
];
public static usage = 'key add <name> [path]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public static readStdin = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
let key: string;
if (params.path != null) {
const { promisify } = await import('util');
const readFileAsync = promisify((await import('fs')).readFile);
key = await readFileAsync(params.path, 'utf8');
} else if (this.stdin.length > 0) {
key = this.stdin;
} else {
throw new ExpectedError('No public key file or path provided.');
}
await getBalenaSdk().models.key.create(params.name, key);
}
}

View File

@ -0,0 +1,79 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* 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.
*/
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyCmd extends Command {
public static description = stripIndent`
Display an SSH key.
Display a single SSH key registered in balenaCloud for the logged in user.
`;
public static examples = ['$ balena key 17'];
public static args: Array<IArg<any>> = [
{
name: 'id',
description: 'balenaCloud ID for the SSH key',
parse: x => parseAsInteger(x, 'id'),
required: true,
},
];
public static usage = 'key <id>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
const key = await getBalenaSdk().models.key.get(params.id);
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
}
}

View File

@ -0,0 +1,79 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* 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.
*/
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyRmCmd extends Command {
public static description = stripIndent`
Remove an SSH key from balenaCloud.
Remove a single SSH key registered in balenaCloud for the logged in user.
The --yes option may be used to avoid interactive confirmation.
`;
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
public static args: Array<IArg<any>> = [
{
name: 'id',
description: 'balenaCloud ID for the SSH key',
parse: x => parseAsInteger(x, 'id'),
required: true,
},
];
public static usage = 'key rm <id>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyRmCmd,
);
const patterns = await import('../../utils/patterns');
await patterns.confirm(
options.yes ?? false,
`Are you sure you want to delete key ${params.id}?`,
);
await getBalenaSdk().models.key.remove(params.id);
}
}

56
lib/actions-oclif/keys.ts Normal file
View File

@ -0,0 +1,56 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* 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.
*/
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
interface FlagsDef {
help: void;
}
export default class KeysCmd extends Command {
public static description = stripIndent`
List the SSH keys in balenaCloud.
List all SSH keys registered in balenaCloud for the logged in user.
`;
public static examples = ['$ balena keys'];
public static usage = 'keys';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();
// Use 'name' instead of 'title' to match dashboard.
const displayKeys: Array<{ id: number; name: string }> = keys.map(k => {
return { id: k.id, name: k.title };
});
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
}
}

View File

@ -20,7 +20,6 @@ module.exports =
auth: require('./auth')
device: require('./device')
tags: require('./tags')
keys: require('./keys')
logs: require('./logs')
local: require('./local')
scan: require('./scan')

View File

@ -1,129 +0,0 @@
/*
Copyright 2016-2020 Balena Ltd.
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.
*/
import { CommandDefinition } from 'capitano';
import { ExpectedError } from '../errors';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
import * as commandOptions from './command-options';
function parseId(id: string): number {
if (/^[\d]+$/.exec(id) == null) {
throw new ExpectedError('The key id must be an integer');
}
return Number(id);
}
export const list: CommandDefinition = {
signature: 'keys',
description: 'list all ssh keys',
help: `\
Use this command to list all your SSH keys.
Examples:
$ balena keys\
`,
permission: 'user',
async action() {
const keys = await getBalenaSdk().models.key.getAll();
console.log(getVisuals().table.horizontal(keys, ['id', 'title']));
},
};
export const info: CommandDefinition<{ id: string }> = {
signature: 'key <id>',
description: 'list a single ssh key',
help: `\
Use this command to show information about a single SSH key.
Examples:
$ balena key 17\
`,
permission: 'user',
async action(params) {
const key = await getBalenaSdk().models.key.get(parseId(params.id));
console.log(getVisuals().table.vertical(key, ['id', 'title']));
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
},
};
export const remove: CommandDefinition<
{ id: string },
commandOptions.YesOption
> = {
signature: 'key rm <id>',
description: 'remove a ssh key',
help: `\
Use this command to remove a SSH key from balena.
Notice this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
Examples:
$ balena key rm 17
$ balena key rm 17 --yes\
`,
options: [commandOptions.yes],
permission: 'user',
async action(params, options) {
const patterns = await import('../utils/patterns');
await patterns.confirm(
options.yes ?? false,
'Are you sure you want to delete the key?',
);
await getBalenaSdk().models.key.remove(parseId(params.id));
},
};
export const add: CommandDefinition<{ name: string; path: string }> = {
signature: 'key add <name> [path]',
description: 'add a SSH key to balena',
help: `\
Use this command to associate a new SSH key with your account.
If \`path\` is omitted, the command will attempt
to read the SSH key from stdin.
Examples:
$ balena key add Main ~/.ssh/id_rsa.pub
$ cat ~/.ssh/id_rsa.pub | balena key add Main\
`,
permission: 'user',
async action(params) {
let key: string;
if (params.path != null) {
const { promisify } = await import('util');
const readFileAsync = promisify((await import('fs')).readFile);
key = await readFileAsync(params.path, 'utf8');
} else {
const getStdin = await import('get-stdin');
key = await getStdin();
}
await getBalenaSdk().models.key.create(params.name, key);
},
};

View File

@ -74,12 +74,6 @@ capitano.command(actions.device.move)
capitano.command(actions.device.osUpdate)
capitano.command(actions.device.info)
# ---------- Keys Module ----------
capitano.command(actions.keys.list)
capitano.command(actions.keys.add)
capitano.command(actions.keys.info)
capitano.command(actions.keys.remove)
# ---------- Tags Module ----------
capitano.command(actions.tags.list)
capitano.command(actions.tags.set)

View File

@ -134,6 +134,10 @@ export const convertedCommands = [
'internal:scandevices',
'internal:osinit',
'join',
'keys',
'key',
'key:add',
'key:rm',
'leave',
'note',
'os:configure',

View File

@ -46,3 +46,8 @@ export const verbose: IBooleanFlag<boolean> = flags.boolean({
char: 'v',
description: 'produce verbose output',
});
export const yes: IBooleanFlag<boolean> = flags.boolean({
char: 'y',
description: 'answer "yes" to all questions (non interactive use)',
});

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2017 Balena
Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,6 +15,7 @@ limitations under the License.
*/
import validEmail = require('@resin.io/valid-email');
import { ExpectedError } from '../errors';
const APPNAME_REGEX = new RegExp(/^[a-zA-Z0-9_-]+$/);
// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html
@ -73,3 +74,17 @@ export function validateShortUuid(input: string): boolean {
export function validateUuid(input: string): boolean {
return validateLongUuid(input) || validateShortUuid(input);
}
export function parseAsInteger(input: string, paramName?: string) {
// Allow only digits, no leading 0
if (!/^(0|[1-9][0-9]*)$/.test(input)) {
const message =
paramName == null
? 'The parameter must be an integer.'
: `The parameter '${paramName}' must be an integer.`;
throw new ExpectedError(message);
}
return Number(input);
}

View File

@ -61,10 +61,10 @@ Additional commands:
env rename <id> <value> change the value of a config or env var for an app, device or service
env rm <id> remove a config or env var from an application, device or service
envs list the environment or config variables of an application, device or service
key <id> list a single ssh key
key add <name> [path] add a SSH key to balena
key rm <id> remove a ssh key
keys list all ssh keys
key <id> display an SSH key
key add <name> [path] add an SSH key to balenaCloud
key rm <id> remove an SSH key from balenaCloud
keys list the SSH keys in balenaCloud
local configure <target> (Re)configure a balenaOS drive or image
local flash <image> Flash an image to a drive
logout logout from balena

View File

@ -0,0 +1,54 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* 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.
*/
import { expect } from 'chai';
import { ExpectedError } from '../../build/errors';
import { parseAsInteger } from '../../build/utils/validation';
describe('parseAsInteger() function', function() {
it('should reject non-numeric characters', () => {
expect(() => parseAsInteger('abc')).to.throw(ExpectedError);
expect(() => parseAsInteger('1a')).to.throw(ExpectedError);
expect(() => parseAsInteger('a1')).to.throw(ExpectedError);
expect(() => parseAsInteger('a')).to.throw(ExpectedError);
expect(() => parseAsInteger('1.0')).to.throw(ExpectedError);
});
it('should reject leading zeros', () => {
expect(() => parseAsInteger('01')).to.throw(ExpectedError);
expect(() => parseAsInteger('001')).to.throw(ExpectedError);
});
it('should throw with specific message when param name passed', () => {
expect(() => parseAsInteger('abc')).to.throw(
'The parameter must be an integer.',
);
});
it('should throw with general message when no param name passed', () => {
expect(() => parseAsInteger('abc', 'foo')).to.throw(
"The parameter 'foo' must be an integer.",
);
});
it('should parse integers to number type', () => {
expect(parseAsInteger('100')).to.equal(100);
expect(parseAsInteger('100')).to.be.a('number');
expect(parseAsInteger('0')).to.equal(0);
expect(parseAsInteger('0')).to.be.a('number');
});
});