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

WIP: Download local update bundles #2822

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion completion/_balena
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down
2 changes: 1 addition & 1 deletion completion/balena-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
21 changes: 21 additions & 0 deletions docs/balena-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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).
Expand Down
46 changes: 37 additions & 9 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 44 additions & 0 deletions src/commands/device/download-update.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 114 additions & 0 deletions src/utils/download-update.ts
Original file line number Diff line number Diff line change
@@ -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';

Check failure on line 7 in src/utils/download-update.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (self-hosted, X64)

Cannot find module '@balena/update-bundle' or its corresponding type declarations.

Check failure on line 7 in src/utils/download-update.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (self-hosted, ARM64)

Cannot find module '@balena/update-bundle' or its corresponding type declarations.

Check failure on line 7 in src/utils/download-update.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (macos-12)

Cannot find module '@balena/update-bundle' or its corresponding type declarations.

Check failure on line 7 in src/utils/download-update.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (windows-2019)

Cannot find module '@balena/update-bundle' or its corresponding type declarations.

Check failure on line 7 in src/utils/download-update.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (macos-latest-xlarge)

Cannot find module '@balena/update-bundle' or its corresponding type declarations.

Check failure on line 7 in src/utils/download-update.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test npm (20.x)

Cannot find module '@balena/update-bundle' or its corresponding type declarations.

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