mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-19 21:57:51 +00:00
WIP: Download local update bundles
Change-type: minor Signed-off-by: Zahari Petkov <zahari@balena.io>
This commit is contained in:
parent
0591f5edbd
commit
321dcef638
@ -14,7 +14,7 @@ _balena() {
|
||||
app_cmds=( create )
|
||||
block_cmds=( create )
|
||||
config_cmds=( generate inject read reconfigure write )
|
||||
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet )
|
||||
device_cmds=( deactivate download-update identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet )
|
||||
devices_cmds=( supported )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||
|
@ -13,7 +13,7 @@ _balena_complete()
|
||||
app_cmds="create"
|
||||
block_cmds="create"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet"
|
||||
device_cmds="deactivate download-update identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet"
|
||||
devices_cmds="supported"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||
|
@ -196,6 +196,7 @@ are encouraged to regularly update the balena CLI to the latest version.
|
||||
- Devices
|
||||
|
||||
- [device deactivate <uuid>](#device-deactivate-uuid)
|
||||
- [device download-update <uuid(s)>](#device-download-update-uuid-s)
|
||||
- [device identify <uuid>](#device-identify-uuid)
|
||||
- [device <uuid>](#device-uuid)
|
||||
- [device init](#device-init)
|
||||
@ -1218,6 +1219,26 @@ the UUID of the device to be deactivated
|
||||
|
||||
answer "yes" to all questions (non interactive use)
|
||||
|
||||
## device download-update <uuid(s)>
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device download-update fd3a6a1,e573ad5
|
||||
|
||||
### Arguments
|
||||
|
||||
#### UUIDS
|
||||
|
||||
comma-separated list (no blank spaces) of device UUIDs
|
||||
|
||||
### Options
|
||||
|
||||
#### -o, --output OUTPUT
|
||||
|
||||
output path
|
||||
|
||||
## device identify <uuid>
|
||||
|
||||
Identify a device by making the ACT LED blink (Raspberry Pi).
|
||||
|
46
npm-shrinkwrap.json
generated
46
npm-shrinkwrap.json
generated
@ -14,6 +14,7 @@
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@balena/update-bundle": "^0.5.0",
|
||||
"@oclif/core": "^4.0.8",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
@ -180,6 +181,29 @@
|
||||
"windosu": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"../balena-update-bundle": {
|
||||
"name": "@balena/update-bundle",
|
||||
"version": "0.5.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@balena/resource-bundle": "file:../balena-resource-bundle"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^7.2.1",
|
||||
"@types/chai-as-promised": "^7.1.3",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
|
||||
@ -1676,6 +1700,10 @@
|
||||
"through": "^2.3.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@balena/update-bundle": {
|
||||
"resolved": "../balena-update-bundle",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@ -3894,9 +3922,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz",
|
||||
"integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==",
|
||||
"version": "20.16.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.4.tgz",
|
||||
"integrity": "sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@ -5681,9 +5709,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balena-sdk/node_modules/@types/node": {
|
||||
"version": "18.19.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz",
|
||||
"integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==",
|
||||
"version": "18.19.49",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.49.tgz",
|
||||
"integrity": "sha512-ALCeIR6n0nQ7j0FUF1ycOhrp6+XutJWqEu/vtdEqXFUQwkBfgUA5cEg3ZNmjWGF/ZYA/FcF9QMkL55Ar0O6UrA==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
@ -14324,9 +14352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/yaml": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
||||
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
@ -196,6 +196,7 @@
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"@balena/env-parsing": "^1.1.8",
|
||||
"@balena/es-version": "^1.0.1",
|
||||
"@balena/update-bundle": "^0.5.0",
|
||||
"@oclif/core": "^4.0.8",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@sentry/node": "^6.16.1",
|
||||
|
44
src/commands/device/download-update.ts
Normal file
44
src/commands/device/download-update.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
|
||||
import { downloadUpdateBundle } from '../../utils/download-update';
|
||||
|
||||
export default class DeviceDownloadUpdateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Downloads a device local update bundle.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena device download-update fd3a6a1,e573ad5'];
|
||||
|
||||
public static args = {
|
||||
uuids: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device download-update <uuid(s)>';
|
||||
|
||||
public static flags = {
|
||||
output: Flags.string({
|
||||
description: 'output path',
|
||||
char: 'o',
|
||||
required: true,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(
|
||||
DeviceDownloadUpdateCmd,
|
||||
);
|
||||
|
||||
// TODO: support UUIDs passed through stdin pipe
|
||||
|
||||
await downloadUpdateBundle(params.uuids, options.output);
|
||||
}
|
||||
}
|
114
src/utils/download-update.ts
Normal file
114
src/utils/download-update.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type * as stream from 'stream';
|
||||
import * as zlib from 'zlib';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
import * as bundle from '@balena/update-bundle';
|
||||
|
||||
import { getBalenaSdk } from './lazy';
|
||||
|
||||
enum UpdateBundleFormat {
|
||||
CompressedTar,
|
||||
Tar,
|
||||
}
|
||||
|
||||
export async function downloadUpdateBundle(uuids: string, output: string) {
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const deviceUuids = await deviceUuidsFromParam(balena, uuids);
|
||||
|
||||
const subject = (await balena.auth.getUserInfo()).username;
|
||||
const token = await balena.auth.getToken();
|
||||
|
||||
const updateBundleStream = await bundle.create({
|
||||
type: 'Device',
|
||||
deviceUuids,
|
||||
auth: {
|
||||
scheme: 'Bearer',
|
||||
subject,
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: accept fleet arguments for download update bundle as well
|
||||
/*
|
||||
const updateBundleStream = await bundle.create({
|
||||
type: 'Fleet',
|
||||
appUuid: 'f48afafee22245209acfbaf4c2b482e8',
|
||||
releaseUuid: '65a9cb86a59fbe58430715a3d687ea68',
|
||||
auth: {
|
||||
scheme: 'Bearer',
|
||||
subject,
|
||||
token,
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
await saveBundle(updateBundleStream, output);
|
||||
}
|
||||
|
||||
async function saveBundle(
|
||||
updateBundleStream: stream.Readable,
|
||||
filePath: string,
|
||||
) {
|
||||
const target = fs.createWriteStream(filePath);
|
||||
|
||||
const format = getUpdateBundleFormat(filePath);
|
||||
|
||||
if (format === UpdateBundleFormat.CompressedTar) {
|
||||
const gzip = zlib.createGzip();
|
||||
|
||||
await pipeline(updateBundleStream, gzip, target);
|
||||
} else {
|
||||
await pipeline(updateBundleStream, target);
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdateBundleFormat(filePath: string): UpdateBundleFormat {
|
||||
const ext = getCompoundExtension(filePath);
|
||||
|
||||
if (ext === '.tar.gz' || ext === '.tgz') {
|
||||
return UpdateBundleFormat.CompressedTar;
|
||||
} else if (ext === '.tar') {
|
||||
return UpdateBundleFormat.Tar;
|
||||
} else {
|
||||
throw new Error(`Unsupported file extension: ${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getCompoundExtension(filePath: string): string {
|
||||
let compoundExt = '';
|
||||
let currentPath = filePath;
|
||||
|
||||
for (;;) {
|
||||
const ext = path.extname(currentPath);
|
||||
if (ext === '') {
|
||||
break;
|
||||
}
|
||||
compoundExt = ext + compoundExt;
|
||||
currentPath = path.basename(currentPath, ext);
|
||||
}
|
||||
|
||||
return compoundExt;
|
||||
}
|
||||
|
||||
async function deviceUuidsFromParam(
|
||||
balena: ReturnType<typeof getBalenaSdk>,
|
||||
uuids: string,
|
||||
): Promise<string[]> {
|
||||
const uuidsList = uuids.split(',');
|
||||
|
||||
const deviceUuids = [];
|
||||
for (const uuid of uuidsList) {
|
||||
try {
|
||||
const device = await balena.models.device.get(uuid);
|
||||
deviceUuids.push(device.uuid);
|
||||
} catch (err) {
|
||||
// TODO: Needs proper error handling
|
||||
throw new Error(`UUID error ${uuid}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return deviceUuids;
|
||||
}
|
Loading…
Reference in New Issue
Block a user