Merge pull request #1529 from balena-io/1153-envs-microservices

Add multicontainer (microservices) support for 'balena env(s)'
This commit is contained in:
Paulo Castro 2019-12-13 00:52:57 +00:00 committed by GitHub
commit 658b0a5233
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1497 additions and 215 deletions

View File

@ -586,98 +586,179 @@ Examples:
## envs
List the environment or config variables of an application or device,
as selected by the respective command-line options.
List the environment or configuration variables of an application, device or
service, as selected by the respective command-line options. (A service is
an application container in a "microservices" application.)
The --config option is used to list "configuration variables" that
control balena features.
The --config option is used to list "configuration variables" that control
balena platform features, as opposed to custom environment variables defined
by the user. The --config and the --service options are mutually exclusive
because configuration variables cannot be set for specific services.
Service-specific variables are not currently supported. The following
examples list variables that apply to all services in an app or device.
The --all option is used to include application-wide (fleet), device-wide
(multiple services on a device) and service-specific variables that apply to
the selected application, device or service. It can be thought of as including
"inherited" variables: for example, a service inherits device-wide variables,
and a device inherits application-wide variables. Variables are still filtered
out by type with the --config option, such that configuration and non-
configuration variables are never listed together.
When the --all option is used, the printed output may include DEVICE and/or
SERVICE columns to distinguish between application-wide, device-specific and
service-specific variables. As asterisk in these columns indicates that the
variable applies to "all devices" or "all services".
If you are parsing the output in a script, please select the JSON format with
the '-j' option. This avoids future compatibility issues if columns are added,
renamed or reordered. Also, when the JSON format is selected, an empty JSON
array ([]) is printed instead of an error message when no variables exist for
the given query. When querying variables for a device, note that the application
name may be null in JSON output (or 'N/A' in tabular output) if the application
linked to the device is no longer accessible by the current user (for example,
in case the current user has been removed from the application by its owner).
Examples:
$ balena envs --application MyApp
$ balena envs --application MyApp --all --json
$ balena envs --application MyApp --service MyService
$ balena envs --application MyApp --all --service MyService
$ balena envs --application MyApp --config
$ balena envs --device 7cf02a6
$ balena envs --device 7cf02a6 --all --json
$ balena envs --device 7cf02a6 --config --all --json
$ balena envs --device 7cf02a6 --all --service MyService
### Options
#### --all
include app-wide, device-wide variables that apply to the selected device or service.
Variables are still filtered out by type with the --config option.
#### -a, --application APPLICATION
application name
#### -c, --config
show config variables
show configuration variables only
#### -d, --device DEVICE
device UUID
#### -j, --json
produce JSON output instead of tabular output
#### -v, --verbose
produce verbose output
#### -s, --service SERVICE
service name
## env rm ID
Remove a configuration or environment variable from an application or device,
as selected by command-line options.
Remove a configuration or environment variable from an application, device
or service, as selected by command-line options.
Note that this command asks for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Variables are selected by their database ID (as reported by the 'balena envs'
command) and one of six database "resource types":
The --device option selects a device instead of an application.
The --config option selects a config var instead of an env var.
- application (fleet) environment variable
- application (fleet) configuration variable (--config)
- application (fleet) service variable (--service)
- device environment variable (--device)
- device configuration variable (--device --config)
- device service variable (--device --service)
Service-specific variables are not currently supported. The following
examples remove variables that apply to all services in an app or device.
The --device option selects a device-specific variable instead of an application
(fleet) variable.
The --config option selects a configuration variable. Configuration variable
names typically start with the 'BALENA_' or 'RESIN_' prefixes and are used to
configure balena platform features.
The --service option selects a service variable, which is an environment variable
that applies to a specifc service (application container) in a microservices
(multicontainer) application.
The --service and --config options cannot be used together, but they can be
used alongside the --device option to select a device-specific service or
configuration variable.
Interactive confirmation is normally asked before the variable is deleted.
The --yes option disables this behaviour.
Examples:
$ balena env rm 215
$ balena env rm 215 --yes
$ balena env rm 215 --config
$ balena env rm 215 --device
$ balena env rm 215 --device --config
$ balena env rm 123123
$ balena env rm 234234 --yes
$ balena env rm 345345 --config
$ balena env rm 456456 --service
$ balena env rm 567567 --device
$ balena env rm 678678 --device --config
$ balena env rm 789789 --device --service --yes
### Arguments
#### ID
environment variable numeric database ID
variable's numeric database ID
### Options
#### -d, --device
Selects a device environment variable instead of an application environment variable
#### -c, --config
Selects a configuration variable instead of an environment variable
select a configuration variable (may be used together with the --device option)
#### -d, --device
select a device-specific variable instead of an application (fleet) variable
#### -s, --service
select a service variable (may be used together with the --device option)
#### -y, --yes
Run in non-interactive mode
do not prompt for confirmation before deleting the variable
## env add NAME [VALUE]
Add an environment or config variable to an application or device, as selected
by the respective command-line options.
Add an environment or config variable to an application, device or service,
as selected by the respective command-line options. Either the --application
or the --device option must be provided, and either may be be used alongside
the --service option to define a service-specific variable. (A service is an
application container in a "microservices" application.) When the --service
option is used in conjunction with the --device option, the service variable
applies to the selected device only. Otherwise, it applies to all devices of
the selected application (i.e., the application's fleet). If the --service
option is omitted, the variable applies to all services.
If VALUE is omitted, the CLI will attempt to use the value of the environment
variable of same name in the CLI process' environment. In this case, a warning
message will be printed. Use `--quiet` to suppress it.
Service-specific variables are not currently supported. The given command line
examples variables that apply to all services in an app or device.
'BALENA_' or 'RESIN_' are reserved variable name prefixes used to identify
"configuration variables". Configuration variables control balena platform
features and are treated specially by balenaOS and the balena supervisor
running on devices. They are also stored differently in the balenaCloud API
database. Configuration variables cannot be set for specific services,
therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom application variables, please
avoid the reserved prefixes.
Examples:
$ balena env add TERM --application MyApp
$ balena env add EDITOR vim --application MyApp
$ balena env add EDITOR vim --application MyApp --service MyService
$ balena env add EDITOR vim --device 7cf02a6
$ balena env add EDITOR vim --device 7cf02a6 --service MyService
### Arguments
@ -687,7 +768,7 @@ environment or config variable name
#### VALUE
variable value; if omitted, use value from CLI's environment
variable value; if omitted, use value from this process' environment
### Options
@ -703,36 +784,72 @@ device UUID
suppress warning messages
#### -s, --service SERVICE
service name
## env rename ID VALUE
Change the value of an environment variable for an application or device,
as selected by the '--device' option. The variable is identified by its
database ID, rather than its name. The 'balena envs' command can be used
to list the variable's ID.
Change the value of a configuration or environment variable for an application,
device or service, as selected by command-line options.
Service-specific variables are not currently supported. The following
examples modify variables that apply to all services in an app or device.
Variables are selected by their database ID (as reported by the 'balena envs'
command) and one of six database "resource types":
- application (fleet) environment variable
- application (fleet) configuration variable (--config)
- application (fleet) service variable (--service)
- device environment variable (--device)
- device configuration variable (--device --config)
- device service variable (--device --service)
The --device option selects a device-specific variable instead of an application
(fleet) variable.
The --config option selects a configuration variable. Configuration variable
names typically start with the 'BALENA_' or 'RESIN_' prefixes and are used to
configure balena platform features.
The --service option selects a service variable, which is an environment variable
that applies to a specifc service (application container) in a microservices
(multicontainer) application.
The --service and --config options cannot be used together, but they can be
used alongside the --device option to select a device-specific service or
configuration variable.
Examples:
$ balena env rename 376 emacs
$ balena env rename 376 emacs --device
$ balena env rename 123123 emacs
$ balena env rename 234234 emacs --service
$ balena env rename 345345 emacs --device
$ balena env rename 456456 emacs --device --service
$ balena env rename 567567 1 --config
$ balena env rename 678678 1 --device --config
### Arguments
#### ID
environment variable numeric database ID
variable's numeric database ID
#### VALUE
variable value; if omitted, use value from CLI's environment
variable value; if omitted, use value from this process' environment
### Options
#### -c, --config
select a configuration variable (may be used together with the --device option)
#### -d, --device
select a device variable instead of an application variable
select a device-specific variable instead of an application (fleet) variable
#### -s, --service
select a service variable (may be used together with the --device option)
# Tags

View File

@ -16,17 +16,20 @@
*/
import { Command, flags } from '@oclif/command';
import * as BalenaSdk from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { CommandHelp } from '../../utils/oclif-utils';
interface FlagsDef {
application?: string;
device?: string;
application?: string; // application name
device?: string; // device UUID
help: void;
quiet: boolean;
service?: string; // service name
}
interface ArgsDef {
@ -36,22 +39,37 @@ interface ArgsDef {
export default class EnvAddCmd extends Command {
public static description = stripIndent`
Add an environment or config variable to an application or device.
Add an environment or config variable to an application, device or service.
Add an environment or config variable to an application or device, as selected
by the respective command-line options.
Add an environment or config variable to an application, device or service,
as selected by the respective command-line options. Either the --application
or the --device option must be provided, and either may be be used alongside
the --service option to define a service-specific variable. (A service is an
application container in a "microservices" application.) When the --service
option is used in conjunction with the --device option, the service variable
applies to the selected device only. Otherwise, it applies to all devices of
the selected application (i.e., the application's fleet). If the --service
option is omitted, the variable applies to all services.
If VALUE is omitted, the CLI will attempt to use the value of the environment
variable of same name in the CLI process' environment. In this case, a warning
message will be printed. Use \`--quiet\` to suppress it.
Service-specific variables are not currently supported. The given command line
examples variables that apply to all services in an app or device.
'BALENA_' or 'RESIN_' are reserved variable name prefixes used to identify
"configuration variables". Configuration variables control balena platform
features and are treated specially by balenaOS and the balena supervisor
running on devices. They are also stored differently in the balenaCloud API
database. Configuration variables cannot be set for specific services,
therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom application variables, please
avoid the reserved prefixes.
`;
public static examples = [
'$ balena env add TERM --application MyApp',
'$ balena env add EDITOR vim --application MyApp',
'$ balena env add EDITOR vim --application MyApp --service MyService',
'$ balena env add EDITOR vim --device 7cf02a6',
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
];
public static args = [
@ -64,7 +82,7 @@ export default class EnvAddCmd extends Command {
name: 'value',
required: false,
description:
"variable value; if omitted, use value from CLI's environment",
"variable value; if omitted, use value from this process' environment",
},
];
@ -77,6 +95,7 @@ export default class EnvAddCmd extends Command {
device: _.assign({ exclusive: ['application'] }, cf.device),
help: cf.help,
quiet: cf.quiet,
service: cf.service,
};
public async run() {
@ -85,14 +104,22 @@ export default class EnvAddCmd extends Command {
);
const cmd = this;
const balena = (await import('balena-sdk')).fromSharedOptions();
const { exitWithExpectedError } = await import('../../utils/patterns');
const { checkLoggedIn } = await import('../../utils/patterns');
if (!options.application && !options.device) {
throw new ExpectedError(
'Either the --application or the --device option must always be used',
);
}
await checkLoggedIn();
if (params.value == null) {
params.value = process.env[params.name];
if (params.value == null) {
throw new Error(
`Environment value not found for variable: ${params.name}`,
`Value not found for environment variable: ${params.name}`,
);
} else if (!options.quiet) {
cmd.warn(
@ -101,12 +128,25 @@ export default class EnvAddCmd extends Command {
}
}
const reservedPrefixes = await getReservedPrefixes();
const reservedPrefixes = await getReservedPrefixes(balena);
const isConfigVar = _.some(reservedPrefixes, prefix =>
_.startsWith(params.name, prefix),
);
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.service) {
if (isConfigVar) {
throw new ExpectedError(stripIndent`
Configuration variables prefixed with "${reservedPrefixes.join(
'" or "',
)}" cannot be set per service.
Hint: remove the --service option or rename the variable.
`);
}
await setServiceVars(balena, params, options);
return;
}
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.application) {
await balena.models.application[varType].set(
options.application,
@ -119,16 +159,78 @@ export default class EnvAddCmd extends Command {
params.name,
params.value,
);
} else {
exitWithExpectedError('You must specify an application or device');
}
}
}
async function getReservedPrefixes(): Promise<string[]> {
const balena = (await import('balena-sdk')).fromSharedOptions();
const settings = await balena.settings.getAll();
/**
* Add service variables for a device or application.
*/
async function setServiceVars(
sdk: BalenaSdk.BalenaSDK,
params: ArgsDef,
options: FlagsDef,
) {
if (options.application) {
const serviceId = await getServiceIdForApp(
sdk,
options.application,
options.service!,
);
await sdk.models.service.var.set(serviceId, params.name, params.value!);
} else {
const { getDeviceAndAppFromUUID } = await import('../../utils/cloud');
const [device, app] = await getDeviceAndAppFromUUID(
sdk,
options.device!,
['id'],
['app_name'],
);
const serviceId = await getServiceIdForApp(
sdk,
app.app_name,
options.service!,
);
await sdk.models.device.serviceVar.set(
device.id,
serviceId,
params.name,
params.value!,
);
}
}
/**
* Return a sevice ID for the given app name and service name.
*/
async function getServiceIdForApp(
sdk: BalenaSdk.BalenaSDK,
appName: string,
serviceName: string,
): Promise<number> {
let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appName, {
$filter: { service_name: serviceName },
});
if (!_.isEmpty(services)) {
serviceId = services[0].id;
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for application ${appName}`,
);
}
return serviceId;
}
/**
* Return an array of variable name prefixes like: [ 'RESIN_', 'BALENA_' ].
* These prefixes can be used to identify "configuration variables".
*/
async function getReservedPrefixes(
balena: BalenaSdk.BalenaSDK,
): Promise<string[]> {
const settings = await balena.settings.getAll();
const response = await balena.request.send({
baseUrl: settings.apiUrl,
url: '/config/vars',

View File

@ -18,12 +18,15 @@ import { Command, flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import * as cf from '../../utils/common-flags';
import * as ec from '../../utils/env-common';
import { CommandHelp } from '../../utils/oclif-utils';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
config: boolean;
device: boolean;
service: boolean;
help: void;
}
@ -34,59 +37,59 @@ interface ArgsDef {
export default class EnvRenameCmd extends Command {
public static description = stripIndent`
Change the value of an environment variable for an app or device.
Change the value of a config or env var for an app, device or service.
Change the value of an environment variable for an application or device,
as selected by the '--device' option. The variable is identified by its
database ID, rather than its name. The 'balena envs' command can be used
to list the variable's ID.
Change the value of a configuration or environment variable for an application,
device or service, as selected by command-line options.
Service-specific variables are not currently supported. The following
examples modify variables that apply to all services in an app or device.
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena env rename 376 emacs',
'$ balena env rename 376 emacs --device',
'$ balena env rename 123123 emacs',
'$ balena env rename 234234 emacs --service',
'$ balena env rename 345345 emacs --device',
'$ balena env rename 456456 emacs --device --service',
'$ balena env rename 567567 1 --config',
'$ balena env rename 678678 1 --device --config',
];
public static args: Array<IArg<any>> = [
{
name: 'id',
required: true,
description: 'environment variable numeric database ID',
parse: input => parseInt(input, 10),
description: "variable's numeric database ID",
parse: input => ec.parseDbId(input),
},
{
name: 'value',
required: true,
description:
"variable value; if omitted, use value from CLI's environment",
"variable value; if omitted, use value from this process' environment",
},
];
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
// hardcoded 'env rename' to avoid oclif's 'env:rename' topic syntax
public static usage =
'env rename ' + new CommandHelp({ args: EnvRenameCmd.args }).defaultUsage();
public static flags: flags.Input<FlagsDef> = {
device: flags.boolean({
char: 'd',
description:
'select a device variable instead of an application variable',
}),
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
help: cf.help,
};
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRenameCmd,
);
const balena = (await import('balena-sdk')).fromSharedOptions();
const { checkLoggedIn } = await import('../../utils/patterns');
await checkLoggedIn();
await balena.pine.patch({
resource: options.device
? 'device_environment_variable'
: 'application_environment_variable',
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
id: params.id,
body: {
value: params.value,

View File

@ -18,11 +18,15 @@
import { Command, flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import * as ec from '../../utils/env-common';
import { CommandHelp } from '../../utils/oclif-utils';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
config: boolean;
device: boolean;
service: boolean;
yes: boolean;
}
@ -32,88 +36,69 @@ interface ArgsDef {
export default class EnvRmCmd extends Command {
public static description = stripIndent`
Remove an environment variable from an application or device.
Remove a config or env var from an application, device or service.
Remove a configuration or environment variable from an application or device,
as selected by command-line options.
Remove a configuration or environment variable from an application, device
or service, as selected by command-line options.
Note that this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
The --device option selects a device instead of an application.
The --config option selects a config var instead of an env var.
Service-specific variables are not currently supported. The following
examples remove variables that apply to all services in an app or device.
Interactive confirmation is normally asked before the variable is deleted.
The --yes option disables this behaviour.
`;
public static examples = [
'$ balena env rm 215',
'$ balena env rm 215 --yes',
'$ balena env rm 215 --config',
'$ balena env rm 215 --device',
'$ balena env rm 215 --device --config',
'$ balena env rm 123123',
'$ balena env rm 234234 --yes',
'$ balena env rm 345345 --config',
'$ balena env rm 456456 --service',
'$ balena env rm 567567 --device',
'$ balena env rm 678678 --device --config',
'$ balena env rm 789789 --device --service --yes',
];
public static args = [
public static args: Array<IArg<any>> = [
{
name: 'id',
required: true,
description: 'environment variable numeric database ID',
description: "variable's numeric database ID",
parse: input => ec.parseDbId(input),
},
];
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
// hardcoded 'env rm' to avoid oclif's 'env:rm' topic syntax
public static usage =
'env rm ' + new CommandHelp({ args: EnvRmCmd.args }).defaultUsage();
public static flags: flags.Input<FlagsDef> = {
device: flags.boolean({
char: 'd',
description:
'Selects a device environment variable instead of an application environment variable',
default: false,
}),
config: flags.boolean({
char: 'c',
description:
'Selects a configuration variable instead of an environment variable',
default: false,
}),
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
yes: flags.boolean({
char: 'y',
description: 'Run in non-interactive mode',
description:
'do not prompt for confirmation before deleting the variable',
default: false,
}),
};
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRmCmd,
);
const balena = (await import('balena-sdk')).fromSharedOptions();
const patterns = await import('../../utils/patterns');
const { checkLoggedIn, confirm } = await import('../../utils/patterns');
if (isNaN(params.id) || !Number.isInteger(Number(params.id))) {
patterns.exitWithExpectedError(
'The environment variable id must be an integer',
);
}
await checkLoggedIn();
await patterns.confirm(
options.yes || false,
await confirm(
opt.yes || false,
'Are you sure you want to delete the environment variable?',
undefined,
true,
);
await balena.pine.delete({
resource: options.device
? options.config
? 'device_config_variable'
: 'device_environment_variable'
: options.config
? 'application_config_variable'
: 'application_environment_variable',
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
id: params.id,
});
}

View File

@ -15,38 +15,90 @@
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
import * as SDK from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { CommandHelp } from '../utils/oclif-utils';
interface FlagsDef {
application?: string;
all?: boolean; // whether to include application-wide, device-wide variables
application?: string; // application name
config: boolean;
device?: string;
device?: string; // device UUID
json: boolean;
help: void;
service?: string; // service name
verbose: boolean;
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
appName?: string | null; // application name
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface DeviceServiceEnvironmentVariableInfo
extends SDK.DeviceServiceEnvironmentVariable {
appName?: string; // application name
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
interface ServiceEnvironmentVariableInfo
extends SDK.ServiceEnvironmentVariable {
appName?: string; // application name
deviceUUID?: string; // device UUID
serviceName?: string; // service name
}
export default class EnvsCmd extends Command {
public static description = stripIndent`
List the environment or config variables of an app or device.
List the environment or config variables of an application, device or service.
List the environment or config variables of an application or device,
as selected by the respective command-line options.
List the environment or configuration variables of an application, device or
service, as selected by the respective command-line options. (A service is
an application container in a "microservices" application.)
The --config option is used to list "configuration variables" that
control balena features.
The --config option is used to list "configuration variables" that control
balena platform features, as opposed to custom environment variables defined
by the user. The --config and the --service options are mutually exclusive
because configuration variables cannot be set for specific services.
Service-specific variables are not currently supported. The following
examples list variables that apply to all services in an app or device.
The --all option is used to include application-wide (fleet), device-wide
(multiple services on a device) and service-specific variables that apply to
the selected application, device or service. It can be thought of as including
"inherited" variables: for example, a service inherits device-wide variables,
and a device inherits application-wide variables. Variables are still filtered
out by type with the --config option, such that configuration and non-
configuration variables are never listed together.
When the --all option is used, the printed output may include DEVICE and/or
SERVICE columns to distinguish between application-wide, device-specific and
service-specific variables. As asterisk in these columns indicates that the
variable applies to "all devices" or "all services".
If you are parsing the output in a script, please select the JSON format with
the '-j' option. This avoids future compatibility issues if columns are added,
renamed or reordered. Also, when the JSON format is selected, an empty JSON
array ([]) is printed instead of an error message when no variables exist for
the given query. When querying variables for a device, note that the application
name may be null in JSON output (or 'N/A' in tabular output) if the application
linked to the device is no longer accessible by the current user (for example,
in case the current user has been removed from the application by its owner).
`;
public static examples = [
'$ balena envs --application MyApp',
'$ balena envs --application MyApp --all --json',
'$ balena envs --application MyApp --service MyService',
'$ balena envs --application MyApp --all --service MyService',
'$ balena envs --application MyApp --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --all --json',
'$ balena envs --device 7cf02a6 --config --all --json',
'$ balena envs --device 7cf02a6 --all --service MyService',
];
public static usage = (
@ -54,42 +106,279 @@ export default class EnvsCmd extends Command {
).trim();
public static flags: flags.Input<FlagsDef> = {
all: flags.boolean({
description: stripIndent`
include app-wide, device-wide variables that apply to the selected device or service.
Variables are still filtered out by type with the --config option.`,
}),
application: _.assign({ exclusive: ['device'] }, cf.application),
config: flags.boolean({
char: 'c',
description: 'show config variables',
description: 'show configuration variables only',
exclusive: ['service'],
}),
device: _.assign({ exclusive: ['application'] }, cf.device),
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
verbose: cf.verbose,
service: _.assign({ exclusive: ['config'] }, cf.service),
};
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
const balena = (await import('balena-sdk')).fromSharedOptions();
const balena = SDK.fromSharedOptions();
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
const { checkLoggedIn } = await import('../utils/patterns');
const variables: EnvironmentVariableInfo[] = [];
await checkLoggedIn();
if (!options.application && !options.device) {
throw new ExpectedError('You must specify an application or device');
}
let appName = options.application;
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
if (options.device) {
const [device, app] = await getDeviceAndMaybeAppFromUUID(
balena,
options.device,
['uuid'],
['app_name'],
);
fullUUID = device.uuid;
if (app) {
appName = app.app_name;
}
}
if (appName && options.service) {
await validateServiceName(balena, options.service, appName);
}
if (options.application || options.all) {
variables.push(...(await getAppVars(balena, appName, options)));
}
if (fullUUID) {
variables.push(
...(await getDeviceVars(balena, fullUUID, appName, options)),
);
}
if (!options.json && _.isEmpty(variables)) {
const target =
(options.service ? `service "${options.service}" of ` : '') +
(options.application
? `application "${options.application}"`
: `device "${options.device}"`);
throw new ExpectedError(`No environment variables found for ${target}`);
}
await this.printVariables(variables, options);
}
protected async printVariables(
varArray: EnvironmentVariableInfo[],
options: FlagsDef,
) {
const visuals = await import('resin-cli-visuals');
const { exitWithExpectedError } = await import('../utils/patterns');
const cmd = this;
const fields = ['id', 'name', 'value'];
let environmentVariables: ApplicationVariable[] | DeviceVariable[];
if (options.application) {
environmentVariables = await balena.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(options.application);
} else if (options.device) {
environmentVariables = await balena.models.device[
options.config ? 'configVar' : 'envVar'
].getAllByDevice(options.device);
if (options.all) {
// Replace undefined app names with 'N/A' or null
varArray = _.map(varArray, (i: EnvironmentVariableInfo) => {
i.appName = i.appName || (options.json ? null : 'N/A');
return i;
});
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
}
if (options.json) {
this.log(
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
);
} else {
return exitWithExpectedError('You must specify an application or device');
this.log(
visuals.table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
}
}
}
if (_.isEmpty(environmentVariables)) {
return exitWithExpectedError('No environment variables found');
}
cmd.log(
visuals.table.horizontal(environmentVariables, ['id', 'name', 'value']),
async function validateServiceName(
sdk: SDK.BalenaSDK,
serviceName: string,
appName: string,
) {
const services = await sdk.models.service.getAllByApplication(appName, {
$filter: { service_name: serviceName },
});
if (_.isEmpty(services)) {
throw new ExpectedError(
`Service "${serviceName}" not found for application "${appName}"`,
);
}
}
/**
* Fetch application-wide config / env / service vars.
* If options.application is undefined, an attempt is made to obtain the
* application name from the device UUID (options.device). If this attempt
* fails because the device does not belong to any application, an emtpy
* array is returned.
*/
async function getAppVars(
sdk: SDK.BalenaSDK,
appName: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const appVars: EnvironmentVariableInfo[] = [];
if (!appName) {
return appVars;
}
if (options.config || options.all || !options.service) {
const vars = await sdk.models.application[
options.config ? 'configVar' : 'envVar'
].getAllByApplication(appName);
fillInInfoFields(vars, appName);
appVars.push(...vars);
}
if (!options.config && (options.service || options.all)) {
const pineOpts: SDK.PineOptionsFor<SDK.ServiceEnvironmentVariable> = {
$expand: {
service: {},
},
};
if (options.service) {
pineOpts.$filter = {
service: {
service_name: options.service,
},
};
}
const serviceVars = await sdk.models.service.var.getAllByApplication(
appName,
pineOpts,
);
fillInInfoFields(serviceVars, appName);
appVars.push(...serviceVars);
}
return appVars;
}
/**
* Fetch config / env / service vars when the '--device' option is provided.
* Precondition: options.device must be defined.
*/
async function getDeviceVars(
sdk: SDK.BalenaSDK,
fullUUID: string,
appName: string | undefined,
options: FlagsDef,
): Promise<EnvironmentVariableInfo[]> {
const printedUUID = options.json ? fullUUID : options.device!;
const deviceVars: EnvironmentVariableInfo[] = [];
if (options.config) {
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceConfigVars, appName, printedUUID);
deviceVars.push(...deviceConfigVars);
} else {
if (options.service || options.all) {
const pineOpts: SDK.PineOptionsFor<
SDK.DeviceServiceEnvironmentVariable
> = {
$expand: {
service_install: {
$expand: 'installs__service',
},
},
};
if (options.service) {
pineOpts.$filter = {
service_install: {
installs__service: { service_name: options.service },
},
};
}
const deviceServiceVars = await sdk.models.device.serviceVar.getAllByDevice(
fullUUID,
pineOpts,
);
fillInInfoFields(deviceServiceVars, appName, printedUUID);
deviceVars.push(...deviceServiceVars);
}
if (!options.service || options.all) {
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
fullUUID,
);
fillInInfoFields(deviceEnvVars, appName, printedUUID);
deviceVars.push(...deviceEnvVars);
}
}
return deviceVars;
}
/**
* For each env var object in varArray, fill in its top-level serviceName
* and deviceUUID fields. An asterisk is used to indicate that the variable
* applies to "all services" or "all devices".
*/
function fillInInfoFields(
varArray:
| EnvironmentVariableInfo[]
| DeviceServiceEnvironmentVariableInfo[]
| ServiceEnvironmentVariableInfo[],
appName?: string,
deviceUUID?: string,
) {
for (const envVar of varArray) {
if ('service' in envVar) {
// envVar is of type ServiceEnvironmentVariableInfo
envVar.serviceName = _.at(envVar as any, 'service[0].service_name')[0];
} else if ('service_install' in envVar) {
// envVar is of type DeviceServiceEnvironmentVariableInfo
envVar.serviceName = _.at(
envVar as any,
'service_install[0].installs__service[0].service_name',
)[0];
}
envVar.appName = appName;
envVar.serviceName = envVar.serviceName || '*';
envVar.deviceUUID = deviceUUID || '*';
}
}
/**
* Transform each object (item) of varArray to preserve only the
* fields (keys) listed in the fields argument.
*/
function stringifyVarArray<T = Dictionary<any>>(
varArray: T[],
fields: string[],
): string {
const transformed = _.map(varArray, (o: Dictionary<any>) =>
_.transform(
o,
(result, value, key) => {
if (fields.includes(key)) {
result[key] = value;
}
},
{},
),
);
return JSON.stringify(transformed, null, 4);
}

View File

@ -112,6 +112,16 @@ export function configureBluebird() {
}
}
/**
* Addresses the console warning:
* (node:49500) MaxListenersExceededWarning: Possible EventEmitter memory
* leak detected. 11 error listeners added. Use emitter.setMaxListeners() to
* increase limit
*/
export function setMaxListeners(maxListeners: number) {
require('events').EventEmitter.defaultMaxListeners = maxListeners;
}
export function globalInit() {
setupRaven();
checkNodeVersion();

View File

@ -15,12 +15,18 @@
* limitations under the License.
*/
import { BalenaSDK, Service } from 'balena-sdk';
import memoize = require('lodash/memoize');
import * as SDK from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
export const serviceIdToName = memoize(
async (sdk: BalenaSDK, serviceId: number): Promise<string | undefined> => {
const serviceName = await sdk.pine.get<Service>({
import { ExpectedError } from '../errors';
export const serviceIdToName = _.memoize(
async (
sdk: SDK.BalenaSDK,
serviceId: number,
): Promise<string | undefined> => {
const serviceName = await sdk.pine.get<SDK.Service>({
resource: 'service',
id: serviceId,
options: {
@ -36,3 +42,67 @@ export const serviceIdToName = memoize(
// Memoize the call based on service id
(_sdk, id) => id.toString(),
);
/**
* Return Device and Application objects for the given device UUID (short UUID
* or full UUID). An error is thrown if the application is not accessible, e.g.
* if the application owner removed the current user as a collaborator (but the
* device still belongs to the current user).
*/
export const getDeviceAndAppFromUUID = _.memoize(
async (
sdk: SDK.BalenaSDK,
deviceUUID: string,
selectDeviceFields?: Array<keyof SDK.Device>,
selectAppFields?: Array<keyof SDK.Application>,
): Promise<[SDK.Device, SDK.Application]> => {
const [device, app] = await getDeviceAndMaybeAppFromUUID(
sdk,
deviceUUID,
selectDeviceFields,
selectAppFields,
);
if (app == null) {
throw new ExpectedError(stripIndent`
Unable to access the application that device ${deviceUUID} belongs to.
Hint: check whether the application owner might have withdrawn access to it.
`);
}
return [device, app];
},
// Memoize the call based on UUID
(_sdk, deviceUUID) => deviceUUID,
);
/**
* Return a Device object and maybe an Application object for the given device
* UUID (short UUID or full UUID). The Application object may be undefined if
* the user / device lost access to the application, e.g. if the application
* owner removed the user as a collaborator (but the device still belongs to
* the current user).
*/
export const getDeviceAndMaybeAppFromUUID = _.memoize(
async (
sdk: SDK.BalenaSDK,
deviceUUID: string,
selectDeviceFields?: Array<keyof SDK.Device>,
selectAppFields?: Array<keyof SDK.Application>,
): Promise<[SDK.Device, SDK.Application | undefined]> => {
const pineOpts = {
$expand: selectAppFields
? { belongs_to__application: { $select: selectAppFields } }
: 'belongs_to__application',
} as SDK.PineOptionsFor<SDK.Device>;
if (selectDeviceFields) {
pineOpts.$select = selectDeviceFields as any;
}
const device = await sdk.models.device.get(deviceUUID, pineOpts);
const apps = device.belongs_to__application as SDK.Application[];
if (_.isEmpty(apps) || _.isEmpty(apps[0])) {
return [device, undefined];
}
return [device, apps[0]];
},
// Memoize the call based on UUID
(_sdk, deviceUUID) => deviceUUID,
);

View File

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

110
lib/utils/env-common.ts Normal file
View File

@ -0,0 +1,110 @@
/**
* @license
* Copyright 2019 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 * as _ from 'lodash';
import { ExpectedError } from '../errors';
type IBooleanFlag<T> = import('@oclif/parser/lib/flags').IBooleanFlag<T>;
export const booleanConfig: IBooleanFlag<boolean> = flags.boolean({
char: 'c',
description:
'select a configuration variable (may be used together with the --device option)',
default: false,
exclusive: ['service'],
});
export const booleanDevice: IBooleanFlag<boolean> = flags.boolean({
char: 'd',
description:
'select a device-specific variable instead of an application (fleet) variable',
default: false,
});
export const booleanService: IBooleanFlag<boolean> = flags.boolean({
char: 's',
description:
'select a service variable (may be used together with the --device option)',
default: false,
exclusive: ['config'],
});
export const rmRenameHelp = stripIndent`
Variables are selected by their database ID (as reported by the 'balena envs'
command) and one of six database "resource types":
- application (fleet) environment variable
- application (fleet) configuration variable (--config)
- application (fleet) service variable (--service)
- device environment variable (--device)
- device configuration variable (--device --config)
- device service variable (--device --service)
The --device option selects a device-specific variable instead of an application
(fleet) variable.
The --config option selects a configuration variable. Configuration variable
names typically start with the 'BALENA_' or 'RESIN_' prefixes and are used to
configure balena platform features.
The --service option selects a service variable, which is an environment variable
that applies to a specifc service (application container) in a microservices
(multicontainer) application.
The --service and --config options cannot be used together, but they can be
used alongside the --device option to select a device-specific service or
configuration variable.
`;
/**
* Return an API database resource name like 'device_config_variable' or
* 'service_environment_variable' given three boolean arguments.
* @param isConfig Whether the resource is a configuration variable
* @param isDevice Whether the resource is a device variable
* @param isService Whether the resource is a service variable
*/
export function getVarResourceName(
isConfig: boolean,
isDevice: boolean,
isService: boolean,
): string {
return isDevice
? isConfig
? 'device_config_variable'
: isService
? 'device_service_environment_variable'
: 'device_environment_variable'
: isConfig
? 'application_config_variable'
: isService
? 'service_environment_variable'
: 'application_environment_variable';
}
/**
* Check that the given string looks like and parses like a decimal integer,
* and return the parsed value.
*/
export function parseDbId(id: string): number {
if (/^[\d]+$/.exec(id) == null) {
throw new ExpectedError("The variable's ID must be an integer");
}
return Number(id);
}

View File

@ -15,9 +15,10 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as nock from 'nock';
class BalenaAPIMock {
export class BalenaAPIMock {
public static basePathPattern = /api\.balena-cloud\.com/;
public readonly scope: nock.Scope;
// Expose `scope` as `expect` to allow for better semantics in tests
@ -51,6 +52,124 @@ class BalenaAPIMock {
nock.restore();
}
public expectTestApp() {
this.scope
.get(/^\/v\d+\/application($|\?)/)
.reply(200, { d: [{ id: 1234567 }] });
}
public expectTestDevice(
fullUUID = 'f63fd7d7812c34c4c14ae023fdff05f5',
inaccessibleApp = false,
) {
const id = 7654321;
this.scope.get(/^\/v\d+\/device($|\?)/).reply(200, {
d: [
{
id,
uuid: fullUUID,
belongs_to__application: inaccessibleApp
? []
: [{ app_name: 'test' }],
},
],
});
}
public expectAppEnvVars() {
this.scope
.get(/^\/v\d+\/application_environment_variable($|\?)/)
.reply(200, {
d: [
{
id: 120101,
name: 'var1',
value: 'var1-val',
},
{
id: 120102,
name: 'var2',
value: '22',
},
],
});
}
public expectAppConfigVars() {
this.scope.get(/^\/v\d+\/application_config_variable($|\?)/).reply(200, {
d: [
{
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
],
});
}
public expectAppServiceVars() {
this.scope
.get(/^\/v\d+\/service_environment_variable($|\?)/)
.reply(function(uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined;
let varArray: any[];
if (serviceName) {
const varObj = appServiceVarsByService[serviceName];
varArray = varObj ? [varObj] : [];
} else {
varArray = _.map(appServiceVarsByService, value => value);
}
return [200, { d: varArray }];
});
}
public expectDeviceEnvVars() {
this.scope.get(/^\/v\d+\/device_environment_variable($|\?)/).reply(200, {
d: [
{
id: 120203,
name: 'var3',
value: 'var3-val',
},
{
id: 120204,
name: 'var4',
value: '44',
},
],
});
}
public expectDeviceConfigVars() {
this.scope.get(/^\/v\d+\/device_config_variable($|\?)/).reply(200, {
d: [
{
id: 120400,
name: 'RESIN_SUPERVISOR_POLL_INTERVAL',
value: '900900',
},
],
});
}
public expectDeviceServiceVars() {
this.scope
.get(/^\/v\d+\/device_service_environment_variable($|\?)/)
.reply(function(uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined;
let varArray: any[];
if (serviceName) {
const varObj = deviceServiceVarsByService[serviceName];
varArray = varObj ? [varObj] : [];
} else {
varArray = _.map(deviceServiceVarsByService, value => value);
}
return [200, { d: varArray }];
});
}
public expectConfigVars() {
this.scope.get('/config/vars').reply(200, {
reservedNames: [],
@ -63,21 +182,28 @@ class BalenaAPIMock {
});
}
// User details are cached in the SDK
// so often we don't know if we can expect the whoami request
public expectOptionalWhoAmI() {
this.scope
.get('/user/v1/whoami')
.optionally()
.reply(200, {
id: 99999,
username: 'testuser',
email: 'testuser@test.com',
});
public expectService(serviceName: string, serviceId = 243768) {
this.scope.get(/^\/v\d+\/service($|\?)/).reply(200, {
d: [{ id: serviceId, service_name: serviceName }],
});
}
public expectMixpanel() {
this.scope.get(/^\/mixpanel\/track/).reply(200, {});
// User details are cached in the SDK
// so often we don't know if we can expect the whoami request
public expectWhoAmI(persist = false, optional = true) {
const get = (persist ? this.scope.persist() : this.scope).get(
'/user/v1/whoami',
);
(optional ? get.optionally() : get).reply(200, {
id: 99999,
username: 'testuser',
email: 'testuser@test.com',
});
}
public expectMixpanel(optional = true) {
const get = this.scope.get(/^\/mixpanel\/track/);
(optional ? get.optionally() : get).reply(200, {});
}
protected handleUnexpectedRequest(req: any) {
@ -89,4 +215,52 @@ class BalenaAPIMock {
}
}
export { BalenaAPIMock };
const appServiceVarsByService: { [key: string]: any } = {
service1: {
id: 120110,
name: 'svar1',
value: 'svar1-value',
service: [
{
id: 210110,
service_name: 'service1',
},
],
},
service2: {
id: 120111,
name: 'svar2',
value: 'svar2-value',
service: [
{
id: 210111,
service_name: 'service2',
},
],
},
};
const deviceServiceVarsByService: { [key: string]: any } = {
service1: {
id: 120120,
name: 'svar3',
value: 'svar3-value',
service: [
{
id: 210110,
service_name: 'service1',
},
],
},
service2: {
id: 120121,
name: 'svar4',
value: 'svar4-value',
service: [
{
id: 210111,
service_name: 'service2',
},
],
},
};

View File

@ -37,7 +37,7 @@ describe('balena app create', function() {
});
it('should print help text with the -h flag', async () => {
api.expectOptionalWhoAmI();
api.expectWhoAmI();
api.expectMixpanel();
const { out, err } = await runCommand('app create -h');

View File

@ -25,7 +25,7 @@ describe('balena devices supported', function() {
});
it('should print help text with the -h flag', async () => {
api.expectOptionalWhoAmI();
api.expectWhoAmI();
api.expectMixpanel();
const { out, err } = await runCommand('devices supported -h');
@ -36,7 +36,7 @@ describe('balena devices supported', function() {
});
it('should list currently supported devices, with correct filtering', async () => {
api.expectOptionalWhoAmI();
api.expectWhoAmI();
api.expectMixpanel();
api.scope

View File

@ -1,29 +1,50 @@
/**
* @license
* Copyright 2019 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 { balenaAPIMock, runCommand } from '../../helpers';
import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena env add', function() {
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectWhoAmI(true);
api.expectMixpanel();
});
afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should successfully add an environment variable', async () => {
const deviceId = 'f63fd7d7812c34c4c14ae023fdff05f5';
const mock = balenaAPIMock();
mock
.get(/device/)
.reply(201, {
d: [
{
id: 1031543,
__metadata: { uri: '/resin/device(@id)?@id=1031543' },
},
],
})
.post(/device_environment_variable/)
api.expectTestDevice();
api.expectConfigVars();
api.scope
.post(/^\/v\d+\/device_environment_variable($|\?)/)
.reply(200, 'OK');
const { out, err } = await runCommand(`env add TEST 1 -d ${deviceId}`);
expect(out.join('')).to.equal('');
expect(err.join('')).to.equal('');
// @ts-ignore
mock.remove();
});
});

356
tests/commands/env/envs.spec.ts vendored Normal file
View File

@ -0,0 +1,356 @@
/**
* @license
* Copyright 2019 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 { stripIndent } from 'common-tags';
import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena envs', function() {
const appName = 'test';
let fullUUID: string;
let shortUUID: string;
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectWhoAmI(true);
api.expectMixpanel();
// Random device UUID used to frustrate _.memoize() in utils/cloud.ts
fullUUID = require('crypto')
.randomBytes(16)
.toString('hex');
shortUUID = fullUUID.substring(0, 7);
});
afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should successfully list env vars for a test app', async () => {
api.expectTestApp();
api.expectAppEnvVars();
const { out, err } = await runCommand(`envs -a ${appName}`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120101 var1 var1-val
120102 var2 22
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list config vars for a test app', async () => {
api.expectTestApp();
api.expectAppConfigVars();
const { out, err } = await runCommand(`envs -a ${appName} --config`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list config vars for a test app (JSON output)', async () => {
api.expectTestApp();
api.expectAppConfigVars();
const { out, err } = await runCommand(`envs -cja ${appName}`);
expect(JSON.parse(out.join(''))).to.deep.equal([
{
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
]);
expect(err.join('')).to.equal('');
});
it('should successfully list service variables for a test app (-s flag)', async () => {
const serviceName = 'service2';
api.expectService(serviceName);
api.expectTestApp();
api.expectAppServiceVars();
const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120111 svar2 svar2-value
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should produce an empty JSON array when no app service variables exist', async () => {
const serviceName = 'nono';
api.expectService(serviceName);
api.expectTestApp();
api.expectAppServiceVars();
const { out, err } = await runCommand(
`envs -a ${appName} -s ${serviceName} -j`,
);
expect(out.join('')).to.equal('[]\n');
expect(err.join('')).to.equal('');
});
it('should successfully list env and service vars for a test app (--all flag)', async () => {
api.expectTestApp();
api.expectAppEnvVars();
api.expectAppServiceVars();
const { out, err } = await runCommand(`envs -a ${appName} --all`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION SERVICE
120110 svar1 svar1-value test service1
120111 svar2 svar2-value test service2
120101 var1 var1-val test *
120102 var2 22 test *
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list env and service vars for a test app (--all -s flags)', async () => {
const serviceName = 'service1';
api.expectService(serviceName);
api.expectTestApp();
api.expectAppEnvVars();
api.expectAppServiceVars();
const { out, err } = await runCommand(
`envs -a ${appName} --all -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION SERVICE
120110 svar1 svar1-value test ${serviceName}
120101 var1 var1-val test *
120102 var2 22 test *
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list env variables for a test device', async () => {
api.expectTestDevice(fullUUID);
api.expectDeviceEnvVars();
const { out, err } = await runCommand(`envs -d ${shortUUID}`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120203 var3 var3-val
120204 var4 44
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list env variables for a test device (JSON output)', async () => {
api.expectTestDevice(fullUUID);
api.expectDeviceEnvVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
expect(JSON.parse(out.join(''))).to.deep.equal([
{
id: 120203,
name: 'var3',
value: 'var3-val',
},
{
id: 120204,
name: 'var4',
value: '44',
},
]);
expect(err.join('')).to.equal('');
});
it('should successfully list config variables for a test device', async () => {
api.expectTestDevice(fullUUID);
api.expectDeviceConfigVars();
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list service variables for a test device (-s flag)', async () => {
const serviceName = 'service2';
api.expectService(serviceName);
api.expectTestApp();
api.expectTestDevice(fullUUID);
api.expectDeviceServiceVars();
const { out, err } = await runCommand(
`envs -d ${shortUUID} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120121 svar4 svar4-value
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should produce an empty JSON array when no device service variables exist', async () => {
const serviceName = 'nono';
api.expectService(serviceName);
api.expectTestApp();
api.expectTestDevice(fullUUID);
api.expectDeviceServiceVars();
const { out, err } = await runCommand(
`envs -d ${shortUUID} -s ${serviceName} -j`,
);
expect(out.join('')).to.equal('[]\n');
expect(err.join('')).to.equal('');
});
it('should successfully list env and service variables for a test device (--all flag)', async () => {
api.expectTestApp();
api.expectAppEnvVars();
api.expectAppServiceVars();
api.expectTestDevice(fullUUID);
api.expectDeviceEnvVars();
api.expectDeviceServiceVars();
const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} --all`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * service1
120111 svar2 svar2-value test * service2
120120 svar3 svar3-value test ${uuid} service1
120121 svar4 svar4-value test ${uuid} service2
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list env and service variables for a test device (unknown app)', async () => {
api.expectTestDevice(fullUUID, true);
api.expectDeviceEnvVars();
api.expectDeviceServiceVars();
const uuid = shortUUID;
const { out, err } = await runCommand(`envs -d ${uuid} --all`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} *
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list env and service vars for a test device (--all -s flags)', async () => {
const serviceName = 'service1';
api.expectService(serviceName);
api.expectTestApp();
api.expectAppEnvVars();
api.expectAppServiceVars();
api.expectTestDevice(fullUUID);
api.expectDeviceEnvVars();
api.expectDeviceServiceVars();
const uuid = shortUUID;
const { out, err } = await runCommand(
`envs -d ${uuid} --all -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE APPLICATION DEVICE SERVICE
120110 svar1 svar1-value test * ${serviceName}
120120 svar3 svar3-value test ${uuid} ${serviceName}
120101 var1 var1-val test * *
120102 var2 22 test * *
120203 var3 var3-val test ${uuid} *
120204 var4 44 test ${uuid} *
` + '\n',
);
expect(err.join('')).to.equal('');
});
it('should successfully list env and service vars for a test device (--all -js flags)', async () => {
const serviceName = 'service1';
api.expectService(serviceName);
api.expectTestApp();
api.expectAppEnvVars();
api.expectAppServiceVars();
api.expectTestDevice(fullUUID);
api.expectDeviceEnvVars();
api.expectDeviceServiceVars();
const { out, err } = await runCommand(
`envs -d ${shortUUID} --all -js ${serviceName}`,
);
expect(JSON.parse(out.join(''))).to.deep.equal(
JSON.parse(`[
{ "id": 120101, "appName": "test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "appName": "test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "appName": "test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
{ "id": 120120, "appName": "test", "deviceUUID": "${fullUUID}", "name": "svar3", "value": "svar3-value", "serviceName": "${serviceName}" },
{ "id": 120203, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var3", "value": "var3-val", "serviceName": "*" },
{ "id": 120204, "appName": "test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`),
);
expect(err.join('')).to.equal('');
});
});

View File

@ -16,19 +16,30 @@
*/
import { expect } from 'chai';
import { balenaAPIMock, runCommand } from '../../helpers';
import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena env rename', function() {
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectWhoAmI(true);
api.expectMixpanel();
});
afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should successfully rename an environment variable', async () => {
const mock = balenaAPIMock();
mock.patch(/device_environment_variable\(376\)/).reply(200, 'OK');
api.scope.patch(/device_environment_variable\(376\)/).reply(200, 'OK');
const { out, err } = await runCommand('env rename 376 emacs --device');
expect(out.join('')).to.equal('');
expect(err.join('')).to.equal('');
// @ts-ignore
mock.remove();
});
});

View File

@ -1,17 +1,45 @@
/**
* @license
* Copyright 2016-2019 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 { balenaAPIMock, runCommand } from '../../helpers';
import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers';
describe('balena env rm', function() {
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectWhoAmI(true);
api.expectMixpanel();
});
afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should successfully delete an environment variable', async () => {
const mock = balenaAPIMock();
mock.delete(/device_environment_variable/).reply(200, 'OK');
api.scope.delete(/device_environment_variable/).reply(200, 'OK');
const { out, err } = await runCommand('env rm 144690 -d -y');
expect(out.join('')).to.equal('');
expect(err.join('')).to.equal('');
// @ts-ignore
mock.remove();
});
});

View File

@ -58,10 +58,10 @@ Additional commands:
device rm <uuid> remove a device
device shutdown <uuid> shutdown a device
devices supported list all supported devices
env add <name> [value] add an environment or config variable to an application or device
env rename <id> <value> change the value of an environment variable for an app or device
env rm <id> remove an environment variable from an application or device
envs list the environment or config variables of an app or device
env add <name> [value] add an environment or config variable to an application, device or service
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

View File

@ -21,9 +21,10 @@ import * as nock from 'nock';
import * as path from 'path';
import * as balenaCLI from '../build/app';
import { configureBluebird } from '../build/app-common';
import { configureBluebird, setMaxListeners } from '../build/app-common';
configureBluebird();
setMaxListeners(25); // it appears that 'nock' adds a bunch of listeners - bug?
export const runCommand = async (cmd: string) => {
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];