Add balena envs '-j' option to produce JSON output

Change-type: minor
Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
Paulo Castro 2019-12-05 17:09:02 +00:00
parent 630d53311a
commit df58ac7673
7 changed files with 265 additions and 11 deletions

View File

@ -615,6 +615,10 @@ show config variables
device UUID
#### -j, --json
produce JSON output instead of tabular output
#### -v, --verbose
produce verbose output

View File

@ -15,7 +15,11 @@
* limitations under the License.
*/
import { Command, flags } from '@oclif/command';
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
import {
ApplicationVariable,
DeviceVariable,
EnvironmentVariableBase,
} from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
@ -27,6 +31,7 @@ interface FlagsDef {
application?: string;
config: boolean;
device?: string;
json: boolean;
help: void;
verbose: boolean;
}
@ -62,6 +67,10 @@ export default class EnvsCmd extends Command {
}),
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,
};
@ -91,8 +100,37 @@ export default class EnvsCmd extends Command {
throw new ExpectedError('No environment variables found');
}
cmd.log(
visuals.table.horizontal(environmentVariables, ['id', 'name', 'value']),
);
const fields = ['id', 'name', 'value'];
if (options.json) {
cmd.log(
stringifyVarArray<EnvironmentVariableBase>(
environmentVariables,
fields,
),
);
} else {
cmd.log(visuals.table.horizontal(environmentVariables, fields));
}
}
}
function stringifyVarArray<T = Dictionary<any>>(
varArray: T[],
fields: string[],
): string {
// Transform each object (item) of varArray to preserve
// only the fields (keys) listed in the fields argument.
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

@ -51,15 +51,75 @@ class BalenaAPIMock {
nock.restore();
}
public expectTestApp() {
this.scope
.get(/^\/v\d+\/application($|\?)/)
.reply(200, { d: [{ id: 1234567 }] });
}
public expectTestDevice() {
this.scope.get(/\/v\d+\/device($|\?)/).reply(200, { d: [{ id: 7654321 }] });
this.scope
.get(/^\/v\d+\/device($|\?)/)
.reply(200, { d: [{ id: 7654321 }] });
}
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 expectDeviceEnvVars() {
this.scope.post(/\/v\d+\/device_environment_variable($|\?)/).reply(201, {
id: 120203,
name: 'var3',
value: 'var3-val',
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 expectAppConfigVars() {
this.scope.get(/^\/v\d+\/application_config_variable($|\?)/).reply(200, {
d: [
{
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
],
});
}
public expectDeviceConfigVars() {
this.scope.get(/^\/v\d+\/device_config_variable($|\?)/).reply(200, {
d: [
{
id: 120400,
name: 'RESIN_SUPERVISOR_POLL_INTERVAL',
value: '900900',
},
],
});
}
@ -92,6 +152,7 @@ class BalenaAPIMock {
const get = this.scope.get(/^\/mixpanel\/track/);
(optional ? get.optionally() : get).reply(200, {});
}
protected handleUnexpectedRequest(req: any) {
console.error(`Unexpected http request!: ${req.path}`);
// Errors thrown here are not causing the tests to fail for some reason.

View File

@ -38,7 +38,9 @@ describe('balena env add', function() {
const deviceId = 'f63fd7d7812c34c4c14ae023fdff05f5';
api.expectTestDevice();
api.expectConfigVars();
api.expectDeviceEnvVars();
api.scope
.post(/^\/v\d+\/device_environment_variable($|\?)/)
.reply(200, 'OK');
const { out, err } = await runCommand(`env add TEST 1 -d ${deviceId}`);

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

@ -0,0 +1,138 @@
/**
* @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';
const deviceUUID = 'f63fd7d7812c34c4c14ae023fdff05f5';
let api: BalenaAPIMock;
beforeEach(() => {
api = new BalenaAPIMock();
api.expectOptionalWhoAmI(true);
api.expectMixpanel(true);
});
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 env vars for a test device', async () => {
api.expectTestDevice();
api.expectDeviceEnvVars();
const { out, err } = await runCommand(`envs -d ${deviceUUID}`);
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 vars for a test device (JSON output)', async () => {
api.expectTestDevice();
api.expectDeviceEnvVars();
const { out, err } = await runCommand(`envs -jd ${deviceUUID}`);
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 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 config vars for a test device', async () => {
api.expectTestDevice();
api.expectDeviceConfigVars();
const { out, err } = await runCommand(`envs -d ${deviceUUID} --config`);
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900
` + '\n',
);
expect(err.join('')).to.equal('');
});
});

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(15); // it appears that using nock adds some listeners
export const runCommand = async (cmd: string) => {
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];