Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

device: Add --json option for JSON output #2692

Merged
merged 1 commit into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/balena-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1261,10 +1261,17 @@ the uuid of the device to identify

Show information about a single device.

The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
better represents data types like arrays, empty strings and null values.
The 'jq' utility may be helpful for querying JSON fields in shell scripts
(https://stedolan.github.io/jq/manual/).

Examples:

$ balena device 7cf02a6
$ balena device 7cf02a6 --view
$ balena device 7cf02a6 --json

### Arguments

Expand All @@ -1274,6 +1281,10 @@ the device uuid

### Options

#### -j, --json

produce JSON output instead of tabular output

#### --view

open device dashboard page
Expand Down
76 changes: 49 additions & 27 deletions lib/commands/device/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { jsonInfo } from '../../utils/messages';

import type { Application, Release } from 'balena-sdk';

Expand All @@ -45,10 +46,13 @@ export default class DeviceCmd extends Command {
Show info about a single device.

Show information about a single device.

${jsonInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena device 7cf02a6',
'$ balena device 7cf02a6 --view',
'$ balena device 7cf02a6 --json',
];

public static args = {
Expand All @@ -61,6 +65,7 @@ export default class DeviceCmd extends Command {
public static usage = 'device <uuid>';

public static flags = {
json: cf.json,
help: cf.help,
view: Flags.boolean({
default: false,
Expand All @@ -76,33 +81,45 @@ export default class DeviceCmd extends Command {

const balena = getBalenaSdk();

const device = (await balena.models.device.get(params.uuid, {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
})) as ExtendedDevice;
const device = (await balena.models.device.get(
params.uuid,
options.json
? {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
...expandForAppName.$expand,
},
}
: {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
},
Comment on lines +86 to +121
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny suggestion so that the $select is still used when the --json flag is used.

Suggested change
options.json
? {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
...expandForAppName.$expand,
},
}
: {
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
},
{
$select: [
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
$expand: {
...expandForAppName.$expand,
...(options.json && {
device_tag: {
$select: ['tag_key', 'value'],
},
})
},
},

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively in case not including the $select was intentional, we could make clear that it was intentional by wrapping it in a ...(!options.json && { $select: [/*...*/] }), along w/ a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thgreasi thanks for the review.

It was intentional not to limit the json response based on the fields used by the table printer. JSON format in other tools is a raw data dump that allows the consumer to determine what data is important, which is one of the primary use cases for it. (See also similar discussion in my first PR).

I wouldn't personally add anything to the code because I think the intent of the code is clear based on the use case and experience, but I respect that it's y'all's codebase and I think you can commit that comment to it if you want.

Hope that helps.

)) as ExtendedDevice;

if (options.view) {
const open = await import('open');
Expand Down Expand Up @@ -166,6 +183,11 @@ export default class DeviceCmd extends Command {
);
}

if (options.json) {
console.log(JSON.stringify(device, null, 4));
return;
}

console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
Expand Down
15 changes: 15 additions & 0 deletions tests/commands/device/device.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,19 @@ describe('balena device', function () {
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
});

it('outputs device as JSON with the -j/--json flag', async () => {
api.scope
.get(/^\/v6\/device\?.+&\$expand=device_tag\(\$select=tag_key,value\)/)
.replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
'Content-Type': 'application/json',
});

const { out, err } = await runCommand('device 27fda508c --json');
expect(err).to.be.empty;
const json = JSON.parse(out.join(''));
expect(json.device_name).to.equal('sparkling-wood');
expect(json.belongs_to__application[0].app_name).to.equal('test app');
expect(json.device_tag[0].tag_key).to.equal('example');
});
});
6 changes: 6 additions & 0 deletions tests/test-data/api-response/device.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"__metadata": {}
}
],
"device_tag": [
{
"tag_key": "example",
"value": "true"
}
],
"id": 1747415,
"is_managed_by__device": null,
"device_name": "sparkling-wood",
Expand Down