WIP: Download local update bundles

Change-type: minor
Signed-off-by: Zahari Petkov <zahari@balena.io>
This commit is contained in:
Zahari Petkov 2024-09-04 14:53:30 +03:00
parent 0591f5edbd
commit 321dcef638
No known key found for this signature in database
GPG Key ID: 600CF8A3DD9D353C
7 changed files with 219 additions and 11 deletions

View File

@ -14,7 +14,7 @@ _balena() {
app_cmds=( create ) app_cmds=( create )
block_cmds=( create ) block_cmds=( create )
config_cmds=( generate inject read reconfigure write ) 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 ) devices_cmds=( supported )
env_cmds=( add rename rm ) env_cmds=( add rename rm )
fleet_cmds=( create pin purge rename restart rm track-latest ) fleet_cmds=( create pin purge rename restart rm track-latest )

View File

@ -13,7 +13,7 @@ _balena_complete()
app_cmds="create" app_cmds="create"
block_cmds="create" block_cmds="create"
config_cmds="generate inject read reconfigure write" 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" devices_cmds="supported"
env_cmds="add rename rm" env_cmds="add rename rm"
fleet_cmds="create pin purge rename restart rm track-latest" fleet_cmds="create pin purge rename restart rm track-latest"

View File

@ -196,6 +196,7 @@ are encouraged to regularly update the balena CLI to the latest version.
- Devices - Devices
- [device deactivate &#60;uuid&#62;](#device-deactivate-uuid) - [device deactivate &#60;uuid&#62;](#device-deactivate-uuid)
- [device download-update &#60;uuid(s)&#62;](#device-download-update-uuid-s)
- [device identify &#60;uuid&#62;](#device-identify-uuid) - [device identify &#60;uuid&#62;](#device-identify-uuid)
- [device &#60;uuid&#62;](#device-uuid) - [device &#60;uuid&#62;](#device-uuid)
- [device init](#device-init) - [device init](#device-init)
@ -1218,6 +1219,26 @@ the UUID of the device to be deactivated
answer "yes" to all questions (non interactive use) answer "yes" to all questions (non interactive use)
## device download-update &#60;uuid(s)&#62;
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 &#60;uuid&#62; ## device identify &#60;uuid&#62;
Identify a device by making the ACT LED blink (Raspberry Pi). Identify a device by making the ACT LED blink (Raspberry Pi).

46
npm-shrinkwrap.json generated
View File

@ -14,6 +14,7 @@
"@balena/dockerignore": "^1.0.2", "@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8", "@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1", "@balena/es-version": "^1.0.1",
"@balena/update-bundle": "^0.5.0",
"@oclif/core": "^4.0.8", "@oclif/core": "^4.0.8",
"@resin.io/valid-email": "^0.1.0", "@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^6.16.1", "@sentry/node": "^6.16.1",
@ -180,6 +181,29 @@
"windosu": "^0.3.0" "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": { "node_modules/@aws-crypto/crc32": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
@ -1676,6 +1700,10 @@
"through": "^2.3.8" "through": "^2.3.8"
} }
}, },
"node_modules/@balena/update-bundle": {
"resolved": "../balena-update-bundle",
"link": true
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -3894,9 +3922,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.16.3", "version": "20.16.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.4.tgz",
"integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", "integrity": "sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
@ -5681,9 +5709,9 @@
} }
}, },
"node_modules/balena-sdk/node_modules/@types/node": { "node_modules/balena-sdk/node_modules/@types/node": {
"version": "18.19.48", "version": "18.19.49",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.49.tgz",
"integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", "integrity": "sha512-ALCeIR6n0nQ7j0FUF1ycOhrp6+XutJWqEu/vtdEqXFUQwkBfgUA5cEg3ZNmjWGF/ZYA/FcF9QMkL55Ar0O6UrA==",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -14324,9 +14352,9 @@
} }
}, },
"node_modules/patch-package/node_modules/yaml": { "node_modules/patch-package/node_modules/yaml": {
"version": "2.5.0", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
}, },

View File

@ -196,6 +196,7 @@
"@balena/dockerignore": "^1.0.2", "@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8", "@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1", "@balena/es-version": "^1.0.1",
"@balena/update-bundle": "^0.5.0",
"@oclif/core": "^4.0.8", "@oclif/core": "^4.0.8",
"@resin.io/valid-email": "^0.1.0", "@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^6.16.1", "@sentry/node": "^6.16.1",

View 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);
}
}

View 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;
}