mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-20 06:07:55 +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 )
|
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 )
|
||||||
|
@ -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"
|
||||||
|
@ -196,6 +196,7 @@ are encouraged to regularly update the balena CLI to the latest version.
|
|||||||
- Devices
|
- Devices
|
||||||
|
|
||||||
- [device deactivate <uuid>](#device-deactivate-uuid)
|
- [device deactivate <uuid>](#device-deactivate-uuid)
|
||||||
|
- [device download-update <uuid(s)>](#device-download-update-uuid-s)
|
||||||
- [device identify <uuid>](#device-identify-uuid)
|
- [device identify <uuid>](#device-identify-uuid)
|
||||||
- [device <uuid>](#device-uuid)
|
- [device <uuid>](#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 <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>
|
## device identify <uuid>
|
||||||
|
|
||||||
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
46
npm-shrinkwrap.json
generated
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
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