Merge pull request #1841 from balena-io/convert-tags

Convert `tags`, `tag set`, `tag rm` to oclif.
This commit is contained in:
bulldozer-balena[bot] 2020-05-28 23:35:47 +00:00 committed by GitHub
commit ac3a688d46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 794 additions and 392 deletions

View File

@ -66,7 +66,11 @@ const capitanoDoc = {
},
{
title: 'Tags',
files: ['build/actions/tags.js'],
files: [
'build/actions-oclif/tags.js',
'build/actions-oclif/tag/rm.js',
'build/actions-oclif/tag/set.js',
],
},
{
title: 'Help and Version',

View File

@ -194,8 +194,8 @@ Users are encouraged to regularly update the balena CLI to the latest version.
- Tags
- [tags](#tags)
- [tag set <tagKey> [value]](#tag-set-tagkey-value)
- [tag rm <tagKey>](#tag-rm-tagkey)
- [tag rm <tagkey>](#tag-rm-tagkey)
- [tag set <tagkey> [value]](#tag-set-tagkey-value)
- Help and Version
@ -991,12 +991,10 @@ select a service variable (may be used together with the --device option)
## tags
Use this command to list all tags for
a particular application, device or release.
List all tags and their values for a particular application,
device or release.
This command lists all application/device/release tags.
Example:
Examples:
$ balena tags --application MyApp
$ balena tags --device 7cf02a6
@ -1005,24 +1003,63 @@ Example:
### Options
#### --application, -a, --app <application>
#### -a, --application APPLICATION
application name
#### --device, -d <device>
#### -d, --device DEVICE
device uuid
device UUID
#### --release, -r <release>
#### -r, --release RELEASE
release id
#### --app APP
same as '--application'
## tag rm <tagKey>
Remove a tag from an application, device or release.
Examples:
$ balena tag rm myTagKey --application MyApp
$ balena tag rm myTagKey --device 7cf02a6
$ balena tag rm myTagKey --release 1234
$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6
### Arguments
#### TAGKEY
the key string of the tag
### Options
#### -a, --application APPLICATION
application name
#### -d, --device DEVICE
device UUID
#### -r, --release RELEASE
release id
#### --app APP
same as '--application'
## tag set <tagKey> [value]
Use this command to set a tag to an application, device or release.
Set a tag on an application, device or release.
You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. When the value isn't
tag, as an extra argument after the tag key. If a value isn't
provided, a tag with an empty value is created.
Examples:
@ -1035,45 +1072,34 @@ Examples:
$ balena tag set myCompositeTag --release 1234
$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6
### Options
### Arguments
#### --application, -a, --app <application>
#### TAGKEY
application name
the key string of the tag
#### --device, -d <device>
#### VALUE
device uuid
#### --release, -r <release>
release id
## tag rm <tagKey>
Use this command to remove a tag from an application, device or release.
Examples:
$ balena tag rm myTagKey --application MyApp
$ balena tag rm myTagKey --device 7cf02a6
$ balena tag rm myTagKey --release 1234
$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6
the optional value associated with the tag
### Options
#### --application, -a, --app <application>
#### -a, --application APPLICATION
application name
#### --device, -d <device>
#### -d, --device DEVICE
device uuid
device UUID
#### --release, -r <release>
#### -r, --release RELEASE
release id
#### --app APP
same as '--application'
# Help and Version
## help [command...]

134
lib/actions-oclif/tag/rm.ts Normal file
View File

@ -0,0 +1,134 @@
/**
* @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';
import { disambiguateReleaseParam } from '../../utils/normalization';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
application?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
tagKey: string;
}
export default class TagRmCmd extends Command {
public static description = stripIndent`
Remove a tag from an application, device or release.
Remove a tag from an application, device or release.
`;
public static examples = [
'$ balena tag rm myTagKey --application MyApp',
'$ balena tag rm myTagKey --device 7cf02a6',
'$ balena tag rm myTagKey --release 1234',
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static args = [
{
name: 'tagKey',
description: 'the key string of the tag',
required: true,
},
];
public static usage = 'tag rm <tagKey>';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TagRmCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
if (options.application) {
return balena.models.application.tags.remove(
tryAsInteger(options.application),
params.tagKey,
);
}
if (options.device) {
return balena.models.device.tags.remove(
tryAsInteger(options.device),
params.tagKey,
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
);
return balena.models.release.tags.remove(releaseParam, params.tagKey);
}
}
protected static missingResourceMessage = stripIndent`
To remove a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tag rm
`;
}

View File

@ -0,0 +1,157 @@
/**
* @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';
import { disambiguateReleaseParam } from '../../utils/normalization';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
application?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
interface ArgsDef {
tagKey: string;
value?: string;
}
export default class TagSetCmd extends Command {
public static description = stripIndent`
Set a tag on an application, device or release.
Set a tag on an application, device or release.
You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. If a value isn't
provided, a tag with an empty value is created.
`;
public static examples = [
'$ balena tag set mySimpleTag --application MyApp',
'$ balena tag set myCompositeTag myTagValue --application MyApp',
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
'$ balena tag set myCompositeTag myTagValue --release 1234',
'$ balena tag set myCompositeTag --release 1234',
'$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static args = [
{
name: 'tagKey',
description: 'the key string of the tag',
required: true,
},
{
name: 'value',
description: 'the optional value associated with the tag',
required: false,
},
];
public static usage = 'tag set <tagKey> [value]';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TagSetCmd,
);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
throw new ExpectedError(TagSetCmd.missingResourceMessage);
}
if (params.value == null) {
params.value = '';
}
if (options.application) {
return balena.models.application.tags.set(
tryAsInteger(options.application),
params.tagKey,
params.value,
);
}
if (options.device) {
return balena.models.device.tags.set(
tryAsInteger(options.device),
params.tagKey,
params.value,
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
);
return balena.models.release.tags.set(
releaseParam,
params.tagKey,
params.value,
);
}
}
protected static missingResourceMessage = stripIndent`
To set a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tag set
`;
}

129
lib/actions-oclif/tags.ts Normal file
View File

@ -0,0 +1,129 @@
/**
* @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, getVisuals } from '../utils/lazy';
import { disambiguateReleaseParam } from '../utils/normalization';
import { tryAsInteger } from '../utils/validation';
interface FlagsDef {
application?: string;
device?: string;
release?: string;
help: void;
app?: string;
}
export default class TagsCmd extends Command {
public static description = stripIndent`
List all tags for an application, device or release.
List all tags and their values for a particular application,
device or release.
`;
public static examples = [
'$ balena tags --application MyApp',
'$ balena tags --device 7cf02a6',
'$ balena tags --release 1234',
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static usage = 'tags';
public static flags: flags.Input<FlagsDef> = {
application: {
...cf.application,
exclusive: ['app', 'device', 'release'],
},
device: {
...cf.device,
exclusive: ['app', 'application', 'release'],
},
release: {
...cf.release,
exclusive: ['app', 'application', 'device'],
},
help: cf.help,
app: flags.string({
description: "same as '--application'",
exclusive: ['application', 'device', 'release'],
}),
};
public static authenticated = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
// Prefer options.application over options.app
options.application = options.application || options.app;
delete options.app;
const balena = getBalenaSdk();
// Check user has specified one of application/device/release
if (!options.application && !options.device && !options.release) {
throw new ExpectedError(this.missingResourceMessage);
}
let tags;
if (options.application) {
tags = await balena.models.application.tags.getAllByApplication(
tryAsInteger(options.application),
);
}
if (options.device) {
tags = await balena.models.device.tags.getAllByDevice(
tryAsInteger(options.device),
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
);
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
}
if (!tags || tags.length === 0) {
throw new ExpectedError('No tags found');
}
console.log(
getVisuals().table.horizontal(tags, ['id', 'tag_key', 'value']),
);
}
protected missingResourceMessage = stripIndent`
To list tags for a resource, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tags
`;
}

View File

@ -23,24 +23,10 @@ import * as logs from './logs';
import * as os from './os';
import * as push from './push';
import * as ssh from './ssh';
import * as tags from './tags';
import * as tunnel from './tunnel';
import * as util from './util';
export {
auth,
device,
tags,
logs,
local,
help,
os,
config,
ssh,
util,
push,
tunnel,
};
export { auth, device, logs, local, help, os, config, ssh, util, push, tunnel };
export { build } from './build';

View File

@ -1,295 +0,0 @@
/*
Copyright 2016-2018 Balena
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 { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
import {
disambiguateReleaseParam,
normalizeUuidProp,
} from '../utils/normalization';
import * as commandOptions from './command-options';
export const list: CommandDefinition<
{},
{
application?: string;
device?: string;
release?: number | string;
release_raw?: string;
}
> = {
signature: 'tags',
description: 'list all resource tags',
help: stripIndent`
Use this command to list all tags for
a particular application, device or release.
This command lists all application/device/release tags.
Example:
$ balena tags --application MyApp
$ balena tags --device 7cf02a6
$ balena tags --release 1234
$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6
`,
options: [
commandOptions.optionalApplication,
commandOptions.optionalDevice,
commandOptions.optionalRelease,
],
permission: 'user',
async action(_params, options) {
normalizeUuidProp(options, 'device');
const Bluebird = await import('bluebird');
const _ = await import('lodash');
const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../errors');
return Bluebird.try<ApplicationTag[] | DeviceTag[] | ReleaseTag[]>(
async () => {
const wrongParametersError = stripIndent`
To list resource tags, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tags
`;
if (
_.filter([options.application, options.device, options.release])
.length !== 1
) {
return exitWithExpectedError(wrongParametersError);
}
if (options.application) {
return balena.models.application.tags.getAllByApplication(
options.application,
);
}
if (options.device) {
return balena.models.device.tags.getAllByDevice(options.device);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
options.release_raw,
);
return balena.models.release.tags.getAllByRelease(releaseParam);
}
// return never, so that TS typings are happy
return exitWithExpectedError(wrongParametersError);
},
).tap(function(environmentVariables) {
if (_.isEmpty(environmentVariables)) {
exitWithExpectedError('No tags found');
}
console.log(
getVisuals().table.horizontal(environmentVariables, [
'id',
'tag_key',
'value',
]),
);
});
},
};
export const set: CommandDefinition<
{
tagKey: string;
value?: string;
},
{
application?: string;
device?: string;
release?: number | string;
release_raw: string;
}
> = {
signature: 'tag set <tagKey> [value]',
description: 'set a resource tag',
help: stripIndent`
Use this command to set a tag to an application, device or release.
You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. When the value isn't
provided, a tag with an empty value is created.
Examples:
$ balena tag set mySimpleTag --application MyApp
$ balena tag set myCompositeTag myTagValue --application MyApp
$ balena tag set myCompositeTag myTagValue --device 7cf02a6
$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6
$ balena tag set myCompositeTag myTagValue --release 1234
$ balena tag set myCompositeTag --release 1234
$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6
`,
options: [
commandOptions.optionalApplication,
commandOptions.optionalDevice,
commandOptions.optionalRelease,
],
permission: 'user',
async action(params, options) {
normalizeUuidProp(options, 'device');
const _ = await import('lodash');
const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../errors');
if (_.isEmpty(params.tagKey)) {
return exitWithExpectedError('No tag key was provided');
}
if (
_.filter([options.application, options.device, options.release])
.length !== 1
) {
return exitWithExpectedError(stripIndent`
To set a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tag set
`);
}
if (params.value == null) {
params.value = '';
}
if (options.application) {
return balena.models.application.tags.set(
options.application,
params.tagKey,
params.value,
);
}
if (options.device) {
return balena.models.device.tags.set(
options.device,
params.tagKey,
params.value,
);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
options.release_raw,
);
return balena.models.release.tags.set(
releaseParam,
params.tagKey,
params.value,
);
}
},
};
export const remove: CommandDefinition<
{
tagKey: string;
},
{
application?: string;
device?: string;
release?: number | string;
release_raw?: string;
}
> = {
signature: 'tag rm <tagKey>',
description: 'remove a resource tag',
help: stripIndent`
Use this command to remove a tag from an application, device or release.
Examples:
$ balena tag rm myTagKey --application MyApp
$ balena tag rm myTagKey --device 7cf02a6
$ balena tag rm myTagKey --release 1234
$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6
`,
options: [
commandOptions.optionalApplication,
commandOptions.optionalDevice,
commandOptions.optionalRelease,
],
permission: 'user',
async action(params, options) {
const _ = await import('lodash');
const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../errors');
if (_.isEmpty(params.tagKey)) {
return exitWithExpectedError('No tag key was provided');
}
if (
_.filter([options.application, options.device, options.release])
.length !== 1
) {
return exitWithExpectedError(stripIndent`
To remove a resource tag, you must provide exactly one of:
* An application, with --application <appname>
* A device, with --device <uuid>
* A release, with --release <id or commit>
See the help page for examples:
$ balena help tag rm
`);
}
if (options.application) {
return balena.models.application.tags.remove(
options.application,
params.tagKey,
);
}
if (options.device) {
return balena.models.device.tags.remove(options.device, params.tagKey);
}
if (options.release) {
const releaseParam = await disambiguateReleaseParam(
balena,
options.release,
options.release_raw,
);
return balena.models.release.tags.remove(releaseParam, params.tagKey);
}
},
};

View File

@ -71,11 +71,6 @@ capitano.command(actions.device.move);
capitano.command(actions.device.osUpdate);
capitano.command(actions.device.info);
// ---------- Tags Module ----------
capitano.command(actions.tags.list);
capitano.command(actions.tags.set);
capitano.command(actions.tags.remove);
// ---------- OS Module ----------
capitano.command(actions.os.versions);
capitano.command(actions.os.download);

View File

@ -159,6 +159,9 @@ export const convertedCommands = [
'note',
'os:configure',
'settings',
'tags',
'tag:rm',
'tag:set',
'version',
'scan',
];

View File

@ -37,6 +37,11 @@ export const quiet: IBooleanFlag<boolean> = flags.boolean({
default: false,
});
export const release = flags.string({
char: 'r',
description: 'release id',
});
export const service = flags.string({
char: 's',
description: 'service name',

View File

@ -16,6 +16,7 @@ limitations under the License.
import { BalenaSDK } from 'balena-sdk';
import _ = require('lodash');
import { ExpectedError } from '../errors';
export function normalizeUuidProp(
params: { [key: string]: any },
@ -27,25 +28,59 @@ export function normalizeUuidProp(
}
}
/**
* Takes a string which may represent one of:
* - Integer release id
* - String uuid, 7, 32, or 62 char
* - String commit hash, 40 char, with short hashes being 7+ chars (more as needed to avoid collisions)
* and returns the correctly typed value (integer|string).
* @param balena balena sdk
* @param release string representation of release reference (id/hash)
*/
export async function disambiguateReleaseParam(
balena: BalenaSDK,
param: string | number,
paramRaw: string | undefined,
release: string,
) {
// the user has passed an argument that was parsed as a string
if (typeof param !== 'number') {
return param;
// Reject empty values or invalid characters
const mixedCaseHex = /^[a-fA-F0-9]+$/;
if (!release || !mixedCaseHex.test(release)) {
throw new ExpectedError('Invalid release parameter');
}
// check whether the argument was indeed an ID
return balena.models.release
.get(param, { $select: 'id' })
.catch(error => {
// we couldn't find a release by id,
// try whether it was a commit with all numeric digits
return balena.models.release
.get(paramRaw || _.toString(param), { $select: 'id' })
.catchThrow(error);
})
.then(({ id }) => id);
// Accepting short hashes of 7,8,9 chars.
const possibleUuidHashLength = [7, 8, 9, 32, 40, 62].includes(release.length);
const hasLeadingZero = release[0] === '0';
const isOnlyNumerical = /^[0-9]+$/.test(release);
// Reject non-numerical values with invalid uuid/hash lengths
if (!isOnlyNumerical && !possibleUuidHashLength) {
throw new ExpectedError('Invalid release parameter');
}
// Reject leading-zero values with invalid uuid/hash lengths
if (hasLeadingZero && !possibleUuidHashLength) {
throw new ExpectedError('Invalid release parameter');
}
// If alphanumeric, or has leading zero must now be uuid/hash.
if (!isOnlyNumerical || hasLeadingZero) {
return release;
}
// Now very likely an integer id (but still could be number only uuid/hash)
// Check integer id with API
try {
return (
await balena.models.release.get(parseInt(release, 10), {
$select: 'id',
})
).id;
} catch (e) {
if (e.name !== 'BalenaReleaseNotFound') {
throw e;
}
}
// Must be a number only uuid/hash (or nonexistent release)
return (await balena.models.release.get(release, { $select: 'id' })).id;
}

View File

@ -58,7 +58,7 @@ export function validateDotLocalUrl(input: string): boolean {
}
export function validateLongUuid(input: string): boolean {
if (input.length !== 32 && input.length !== 64) {
if (input.length !== 32 && input.length !== 62) {
return false;
}
return UUID_REGEX.test(input);
@ -89,12 +89,9 @@ export function parseAsInteger(input: string, paramName?: string) {
return Number(input);
}
export function tryAsInteger(
input: string,
paramName?: string,
): number | string {
export function tryAsInteger(input: string): number | string {
try {
return parseAsInteger(input, paramName);
return parseAsInteger(input);
} catch {
return input;
}

View File

@ -69,9 +69,9 @@ Additional commands:
os initialize <image> initialize an os image
os versions <type> show the available balenaOS versions for the given device type
settings print current settings
tag rm <tagKey> remove a resource tag
tag set <tagKey> [value] set a resource tag
tags list all resource tags
tag rm <tagkey> remove a tag from an application, device or release
tag set <tagkey> [value] set a tag on an application, device or release
tags list all tags for an application, device or release
util available-drives list available drives
version display version information for the balena CLI and/or Node.js
whoami get current username and email address

View File

@ -0,0 +1,209 @@
/**
* @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 { BalenaReleaseNotFound } from 'balena-errors';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { ExpectedError } from '../../build/errors';
import { disambiguateReleaseParam } from '../../build/utils/normalization';
describe('disambiguateReleaseParam() function', () => {
it('should reject empty values', async () => {
try {
await disambiguateReleaseParam(null as any, '');
throw new Error('should not be reached');
} catch (e) {
expect(e).to.be.an.instanceOf(ExpectedError);
expect(e.message).to.equal('Invalid release parameter');
}
});
it('should reject values containing invalid chars', async () => {
const invalidCharExamples = ' .,-_=!@#$%^&*() ';
for (const char of invalidCharExamples) {
try {
await disambiguateReleaseParam(null as any, char);
throw new Error('should not be reached');
} catch (e) {
expect(e).to.be.an.instanceOf(ExpectedError);
expect(e.message).to.equal('Invalid release parameter');
}
}
});
it('should reject non-numerical values with invalid uuid/hash lengths', async () => {
const invalidLengthValue = 'abcd';
try {
await disambiguateReleaseParam(null as any, invalidLengthValue);
throw new Error('should not be reached');
} catch (e) {
expect(e).to.be.an.instanceOf(ExpectedError);
expect(e.message).to.equal('Invalid release parameter');
}
});
it('should reject leading-zero numerical values with invalid uuid/hash lengths', async () => {
const invalidLengthValue = '01234';
try {
await disambiguateReleaseParam(null as any, invalidLengthValue);
throw new Error('should not be reached');
} catch (e) {
expect(e).to.be.an.instanceOf(ExpectedError);
expect(e.message).to.equal('Invalid release parameter');
}
});
it('should return non-numerical values with valid hash lengths as string, without SDK calls', async () => {
const uuid7 = 'a'.repeat(7);
const uuid32 = 'a'.repeat(32);
const uuid62 = 'a'.repeat(62);
const hash8 = 'a'.repeat(8);
const hash9 = 'a'.repeat(9);
const hash40 = 'a'.repeat(40);
expect(await disambiguateReleaseParam(null as any, uuid7)).to.equal(uuid7);
expect(await disambiguateReleaseParam(null as any, uuid32)).to.equal(
uuid32,
);
expect(await disambiguateReleaseParam(null as any, uuid62)).to.equal(
uuid62,
);
expect(await disambiguateReleaseParam(null as any, hash8)).to.equal(hash8);
expect(await disambiguateReleaseParam(null as any, hash9)).to.equal(hash9);
expect(await disambiguateReleaseParam(null as any, hash40)).to.equal(
hash40,
);
});
it('should return numerical, leading zero values with valid uuid/hash lengths as string, without SDK calls', async () => {
const uuid7 = '0' + '1'.repeat(6);
const uuid32 = '0' + '1'.repeat(31);
const uuid62 = '0' + '1'.repeat(61);
const hash8 = '0' + '1'.repeat(7);
const hash9 = '0' + '1'.repeat(8);
const hash40 = '0' + '1'.repeat(39);
expect(await disambiguateReleaseParam(null as any, uuid7)).to.equal(uuid7);
expect(await disambiguateReleaseParam(null as any, uuid32)).to.equal(
uuid32,
);
expect(await disambiguateReleaseParam(null as any, uuid62)).to.equal(
uuid62,
);
expect(await disambiguateReleaseParam(null as any, hash8)).to.equal(hash8);
expect(await disambiguateReleaseParam(null as any, hash9)).to.equal(hash9);
expect(await disambiguateReleaseParam(null as any, hash40)).to.equal(
hash40,
);
});
it('should return id from SDK on first call, if match is found', async () => {
const input = '1234';
const output = 1234;
const getRelease = sinon.stub().returns(Promise.resolve({ id: output }));
const sdk: any = {
models: {
release: {
get: getRelease,
},
},
};
const result = await disambiguateReleaseParam(sdk, input);
expect(result).to.equal(output);
expect(getRelease.calledOnce).to.be.true;
expect(getRelease.getCall(0).args[0]).to.equal(parseInt(input, 10));
});
it('should return id from SDK on second call, if match is found', async () => {
const input = '1234';
const output = 1234;
const getRelease = sinon
.stub()
.onCall(0)
.returns(Promise.reject(new BalenaReleaseNotFound(input)))
.onCall(1)
.returns(Promise.resolve({ id: output }));
const sdk: any = {
models: {
release: {
get: getRelease,
},
},
};
const result = await disambiguateReleaseParam(sdk, input);
expect(result).to.equal(output);
expect(getRelease.calledTwice).to.be.true;
expect(getRelease.getCall(0).args[0]).to.equal(parseInt(input, 10));
expect(getRelease.getCall(1).args[0]).to.equal(input);
});
it('should throw error if no match found', async () => {
const input = '1234';
const getRelease = sinon
.stub()
.returns(Promise.reject(new BalenaReleaseNotFound(input)));
const sdk: any = {
models: {
release: {
get: getRelease,
},
},
};
try {
await disambiguateReleaseParam(sdk, input);
throw new Error('should not be reached');
} catch (e) {
expect(e).to.be.an.instanceOf(BalenaReleaseNotFound);
expect(getRelease.calledTwice).to.be.true;
}
});
it('should throw error if unknown error returned from SDK', async () => {
const input = '1234';
const getRelease = sinon
.stub()
.returns(Promise.reject(new Error('some error')));
const sdk: any = {
models: {
release: {
get: getRelease,
},
},
};
try {
await disambiguateReleaseParam(sdk, input);
throw new Error('should not be reached');
} catch (e) {
expect(e).to.be.an.instanceOf(Error);
expect(e.message).to.equal('some error');
expect(getRelease.calledOnce).to.be.true;
}
});
});

View File

@ -114,17 +114,17 @@ describe('validateDotLocalUrl() function', () => {
});
describe('validateLongUuid() function', () => {
it('should return false for strings with length other than 32 or 64', () => {
it('should return false for strings with length other than 32 or 62', () => {
expect(v.validateLongUuid('')).to.equal(false);
expect(v.validateLongUuid('abc')).to.equal(false);
expect(v.validateLongUuid('a'.repeat(31))).to.equal(false);
expect(v.validateLongUuid('a'.repeat(33))).to.equal(false);
expect(v.validateLongUuid('a'.repeat(63))).to.equal(false);
expect(v.validateLongUuid('a'.repeat(65))).to.equal(false);
expect(v.validateLongUuid('a'.repeat(64))).to.equal(false);
});
it('should return false for strings with characters other than a-z,0-9', () => {
it('should return false for strings with characters other than a-f,0-9', () => {
expect(v.validateLongUuid('a'.repeat(31) + 'A')).to.equal(false);
expect(v.validateLongUuid('a'.repeat(31) + 'g')).to.equal(false);
expect(v.validateLongUuid('a'.repeat(31) + '.')).to.equal(false);
expect(v.validateLongUuid('a'.repeat(31) + '-')).to.equal(false);
expect(v.validateLongUuid('a'.repeat(31) + '_')).to.equal(false);
@ -134,9 +134,7 @@ describe('validateLongUuid() function', () => {
expect(v.validateLongUuid('8ab84942d20b4753e08243a9e3a177e2')).to.equal(
true,
);
expect(
v.validateLongUuid('8ab84942d20b4753e08243a9e3a177e2'.repeat(2)),
).to.equal(true);
expect(v.validateLongUuid('a'.repeat(62))).to.equal(true);
});
});
@ -161,19 +159,19 @@ describe('validateShortUuid() function', () => {
});
describe('validateUuid() function', () => {
it('should return false for strings with length other than 7, 32 or 64', () => {
it('should return false for strings with length other than 7, 32 or 62', () => {
expect(v.validateUuid('')).to.equal(false);
expect(v.validateUuid('abc')).to.equal(false);
expect(v.validateUuid('a'.repeat(6))).to.equal(false);
expect(v.validateUuid('a'.repeat(8))).to.equal(false);
expect(v.validateUuid('a'.repeat(31))).to.equal(false);
expect(v.validateUuid('a'.repeat(33))).to.equal(false);
expect(v.validateUuid('a'.repeat(63))).to.equal(false);
expect(v.validateUuid('a'.repeat(65))).to.equal(false);
expect(v.validateUuid('a'.repeat(64))).to.equal(false);
});
it('should return false for strings with characters other than a-z,0-9', () => {
it('should return false for strings with characters other than a-f,0-9', () => {
expect(v.validateUuid('a'.repeat(31) + 'A')).to.equal(false);
expect(v.validateUuid('a'.repeat(31) + 'g')).to.equal(false);
expect(v.validateUuid('a'.repeat(31) + '.')).to.equal(false);
expect(v.validateUuid('a'.repeat(31) + '-')).to.equal(false);
expect(v.validateUuid('a'.repeat(31) + '_')).to.equal(false);
@ -182,9 +180,7 @@ describe('validateUuid() function', () => {
it('should return true for valid UUIDs', () => {
expect(v.validateUuid('8ab8494')).to.equal(true);
expect(v.validateUuid('8ab84942d20b4753e08243a9e3a177e2')).to.equal(true);
expect(
v.validateUuid('8ab84942d20b4753e08243a9e3a177e2'.repeat(2)),
).to.equal(true);
expect(v.validateLongUuid('a'.repeat(62))).to.equal(true);
});
});
@ -221,3 +217,24 @@ describe('parseAsInteger() function', () => {
expect(v.parseAsInteger('0')).to.be.a('number');
});
});
describe('tryAsInteger() function', () => {
it('should return string with non-numeric characters as string', () => {
expect(v.tryAsInteger('abc')).to.be.a('string');
expect(v.tryAsInteger('1a')).to.be.a('string');
expect(v.tryAsInteger('a1')).to.be.a('string');
expect(v.tryAsInteger('a')).to.be.a('string');
expect(v.tryAsInteger('1.0')).to.be.a('string');
});
it('should return numerical strings with leading zeros as string', () => {
expect(v.tryAsInteger('01')).to.be.a('string');
expect(v.tryAsInteger('001')).to.be.a('string');
});
it('should return numerical strings without leading zeros as number', () => {
expect(v.tryAsInteger('100')).to.be.a('number');
expect(v.tryAsInteger('256')).to.be.a('number');
expect(v.tryAsInteger('0')).to.be.a('number');
});
});