From 58ad003167b92a5ea3705f0ba4d73edb3463712f Mon Sep 17 00:00:00 2001 From: "Carlo Miguel F. Cruz" Date: Mon, 1 Jul 2024 21:20:07 +0800 Subject: [PATCH] Adds commands for exporting and importing app/fleet releases. Change-type: minor Signed-off-by: Carlo Miguel F. Cruz --- completion/_balena | 2 +- completion/balena-completion.bash | 2 +- docs/balena-cli.md | 52 +++++++++++++++ lib/commands/release/export.ts | 98 ++++++++++++++++++++++++++++ lib/commands/release/import.ts | 92 +++++++++++++++++++++++++++ npm-shrinkwrap.json | 102 +++++++++++++++++++++++------- package.json | 1 + 7 files changed, 323 insertions(+), 26 deletions(-) create mode 100644 lib/commands/release/export.ts create mode 100644 lib/commands/release/import.ts diff --git a/completion/_balena b/completion/_balena index eb33194366..0c62cc20c4 100644 --- a/completion/_balena +++ b/completion/_balena @@ -22,7 +22,7 @@ _balena() { key_cmds=( add rm ) local_cmds=( configure flash ) os_cmds=( build-config configure download initialize versions ) - release_cmds=( finalize invalidate validate ) + release_cmds=( export finalize import invalidate validate ) tag_cmds=( rm set ) diff --git a/completion/balena-completion.bash b/completion/balena-completion.bash index 3c1101a1eb..e6d548a772 100644 --- a/completion/balena-completion.bash +++ b/completion/balena-completion.bash @@ -21,7 +21,7 @@ _balena_complete() key_cmds="add rm" local_cmds="configure flash" os_cmds="build-config configure download initialize versions" - release_cmds="finalize invalidate validate" + release_cmds="export finalize import invalidate validate" tag_cmds="rm set" diff --git a/docs/balena-cli.md b/docs/balena-cli.md index 8dd1ae8ffe..e2a98acebb 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -282,7 +282,9 @@ are encouraged to regularly update the balena CLI to the latest version. - Releases + - [release export <commitorid>](#release-export-commitorid) - [release finalize <commitorid>](#release-finalize-commitorid) + - [release import <bundlefile>](#release-import-bundlefile) - [release <commitorid>](#release-commitorid) - [release invalidate <commitorid>](#release-invalidate-commitorid) - [release validate <commitorid>](#release-validate-commitorid) @@ -3369,6 +3371,29 @@ The notes for this release # Releases +## release export <commitOrId> + +Exports a successful release to a release bundle file that can be used + to import the release to another application or fleet. + +Examples: + + $ balena release export a777f7345fe3d655c1c981aa642e5555 -o ../path/to/release.tar + $ balena release export 1234567 -o ../path/to/release.tar + $ balena release export myOrg/myFleet:1.2.3 -o ../path/to/release.tar + +### Arguments + +#### COMMITORID + +commit, ID, or tag of the release to export + +### Options + +#### -o, --output OUTPUT + +output path + ## release finalize <commitOrId> Finalize a release. Releases can be "draft" or "final", and this command @@ -3395,6 +3420,33 @@ the commit or ID of the release to finalize ### Options +## release import <bundleFile> + + + +Examples: + + $ balena release import ../path/to/release.tar -f 1234567 + $ balena release import ../path/to/release.tar -f myFleet + $ balena release import ../path/to/release.tar -f myOrg/myFleet + $ balena release import ../path/to/release.tar -f myOrg/myFleet -O 1.2.3 + +### Arguments + +#### BUNDLE + +path to a release bundle file, e.g. "release.tar" + +### Options + +#### -f, --fleet FLEET + +fleet name or slug (preferred) + +#### -O, --override-version OVERRIDE-VERSION + +Overrides the version of the release bundle + ## release <commitOrId> The --json option is recommended when scripting the output of this command, diff --git a/lib/commands/release/export.ts b/lib/commands/release/export.ts new file mode 100644 index 0000000000..11723c248f --- /dev/null +++ b/lib/commands/release/export.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2016-2024 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { commitOrIdArg } from '.'; +import { Flags } from '@oclif/core'; +import Command from '../../command'; +import * as cf from '../../utils/common-flags'; +import { getBalenaSdk, stripIndent } from '../../utils/lazy'; +import { create } from '@balena/release-bundle'; +import * as fs from 'fs/promises'; + +export default class ReleaseExportCmd extends Command { + public static description = stripIndent` + Exports a release to a release bundle file. + + Exports a successful release to a release bundle file that can be used + to import the release to another application or fleet. +`; + public static examples = [ + '$ balena release export a777f7345fe3d655c1c981aa642e5555 -o ../path/to/release.tar', + '$ balena release export 1234567 -o ../path/to/release.tar', + '$ balena release export myOrg/myFleet:1.2.3 -o ../path/to/release.tar', + ]; + + public static usage = 'release export '; + + public static flags = { + output: Flags.string({ + description: 'output path', + char: 'o', + required: true, + }), + help: cf.help, + }; + + public static args = { + commitOrId: commitOrIdArg({ + description: 'commit, ID, or tag of the release to export', + required: true, + }), + }; + + public static authenticated = true; + + public async run() { + const { args: params, flags: options } = await this.parse(ReleaseExportCmd); + + const balena = getBalenaSdk(); + + let release: balenaSdk.Release; + if ( + typeof params.commitOrId === 'string' && + params.commitOrId.includes(':') + ) { + const fleet = params.commitOrId.split(':')[0]; + const rawVersion = params.commitOrId.split(':')[1]; + release = await balena.models.release.get( + { application: fleet, rawVersion: rawVersion }, + { $select: ['id'], $top: 1 }, + ); + } else { + release = await balena.models.release.get(params.commitOrId, { + $select: ['id'], + $top: 1, + }); + } + + try { + const releaseBundle = await create({ + sdk: balena, + releaseId: release.id, + }); + await fs.writeFile(options.output, releaseBundle); + } catch (error) { + console.log( + `Release ${params.commitOrId} could not be exported. ${error.message}`, + ); + } + + console.log( + `Release ${params.commitOrId} has been exported to ${options.output}.`, + ); + } +} diff --git a/lib/commands/release/import.ts b/lib/commands/release/import.ts new file mode 100644 index 0000000000..5d87688f70 --- /dev/null +++ b/lib/commands/release/import.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2016-2024 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Flags, Args } from '@oclif/core'; +import Command from '../../command'; +import * as cf from '../../utils/common-flags'; +import { getBalenaSdk, stripIndent } from '../../utils/lazy'; +import { apply } from '@balena/release-bundle'; +import { createReadStream } from 'fs'; + +export default class ReleaseImportCmd extends Command { + public static description = stripIndent` + Imports a release from a release bundle file to an application or fleet. +`; + public static examples = [ + '$ balena release import ../path/to/release.tar -f 1234567', + '$ balena release import ../path/to/release.tar -f myFleet', + '$ balena release import ../path/to/release.tar -f myOrg/myFleet', + '$ balena release import ../path/to/release.tar -f myOrg/myFleet -O 1.2.3', + ]; + + public static usage = 'release import '; + + public static flags = { + fleet: { ...cf.fleet, exclusive: ['device'] }, + 'override-version': Flags.string({ + description: 'Overrides the version of the release bundle', + char: 'O', + required: true, + }), + help: cf.help, + }; + + public static args = { + bundle: Args.string({ + required: true, + description: 'path to a release bundle file, e.g. "release.tar"', + }), + }; + + public static authenticated = true; + + public async run() { + const { args: params, flags: options } = await this.parse(ReleaseImportCmd); + + const balena = getBalenaSdk(); + + const bundle = createReadStream(params.bundle); + + try { + if ( + typeof options.fleet !== 'number' && + typeof options.fleet !== 'string' + ) { + throw new Error('Fleet must be a number or slug.'); + } + + // TODO: validate if the path to the release bundle exists + + const application = await balena.models.application.get(options.fleet, { + $select: ['id'], + }); + await apply({ + sdk: balena, + application: application.id, + stream: bundle, + version: options['override-version'], + }); + console.log( + `Release bundle ${params.bundle} has been applied to ${options.fleet}.`, + ); + } catch (error) { + console.log( + `Could not apply release bundle to fleet ${options.fleet}. ${error.message}`, + ); + } + } +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 88fa40a98c..679345d070 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -14,6 +14,7 @@ "@balena/dockerignore": "^1.0.2", "@balena/env-parsing": "^1.1.8", "@balena/es-version": "^1.0.1", + "@balena/release-bundle": "^0.5.0-build-add-apply-version-override-84415da02f1be136ce33aadfc4a911f99019d4b8-1", "@oclif/core": "^3.27.0", "@resin.io/valid-email": "^0.1.0", "@sentry/node": "^6.16.1", @@ -1270,10 +1271,13 @@ "dev": true }, "node_modules/@babel/parser": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", - "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1281,6 +1285,20 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/parser/node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -1296,9 +1314,9 @@ } }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.0.tgz", - "integrity": "sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -1346,9 +1364,9 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.0.tgz", - "integrity": "sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -1649,6 +1667,40 @@ "web-streams-polyfill": "^3.1.0" } }, + "node_modules/@balena/release-bundle": { + "version": "0.5.0-build-add-apply-version-override-84415da02f1be136ce33aadfc4a911f99019d4b8-1", + "resolved": "https://registry.npmjs.org/@balena/release-bundle/-/release-bundle-0.5.0-build-add-apply-version-override-84415da02f1be136ce33aadfc4a911f99019d4b8-1.tgz", + "integrity": "sha512-TwruMKUD1RU6pH6jpE91nLJVJPFnAP61O6XDYi4WjGKTX3GhTYwyCRZwrIE7DWRV+qJ42XgNbm9yhMfjNZwH7A==", + "dependencies": { + "@balena/resource-bundle": "^0.4.1", + "balena-semver": "^2.3.5" + }, + "peerDependencies": { + "balena-sdk": "^19.0.0" + } + }, + "node_modules/@balena/resource-bundle": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@balena/resource-bundle/-/resource-bundle-0.4.4.tgz", + "integrity": "sha512-nZ82s0V3z9sO6SHsWgUcDE4aVfK0qEEJcQBOMLgCB8igxx9Q8QRsh+R5Kgj4/AM8kJ3iEi4YQrp16qsnW9RBFw==", + "dependencies": { + "auth-header": "^1.0.0", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@balena/resource-bundle/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/@balena/udif": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@balena/udif/-/udif-1.1.2.tgz", @@ -4257,9 +4309,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz", - "integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==", + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -5434,6 +5486,11 @@ "node": ">= 4.0.0" } }, + "node_modules/auth-header": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz", + "integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -5673,18 +5730,15 @@ } }, "node_modules/balena-image-manager/node_modules/rimraf": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz", - "integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" }, - "engines": { - "node": "14 >=14.20 || 16 >=16.20 || >=18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -5780,9 +5834,9 @@ } }, "node_modules/balena-sdk/node_modules/@types/node": { - "version": "18.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", - "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "version": "18.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.43.tgz", + "integrity": "sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==", "dependencies": { "undici-types": "~5.26.4" } @@ -18342,9 +18396,9 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, "node_modules/url/node_modules/qs": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", - "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { "side-channel": "^1.0.6" }, diff --git a/package.json b/package.json index b49f107302..67702b535b 100644 --- a/package.json +++ b/package.json @@ -203,6 +203,7 @@ "@balena/dockerignore": "^1.0.2", "@balena/env-parsing": "^1.1.8", "@balena/es-version": "^1.0.1", + "@balena/release-bundle": "^0.5.0-build-add-apply-version-override-84415da02f1be136ce33aadfc4a911f99019d4b8-1", "@oclif/core": "^3.27.0", "@resin.io/valid-email": "^0.1.0", "@sentry/node": "^6.16.1",