From 423255d89219b05d6f5ca116ba1c4ee62f00ebcd 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 | 53 ++++++++++++++ lib/commands/release/export.ts | 108 ++++++++++++++++++++++++++++ lib/commands/release/import.ts | 97 +++++++++++++++++++++++++ npm-shrinkwrap.json | 115 ++++++++++++++++++++++-------- package.json | 1 + 7 files changed, 348 insertions(+), 30 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..512d99dedb 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,34 @@ the commit or ID of the release to finalize ### Options +## release import <bundleFile> + +The --override-version option is used to specify the version to be used instead +of using the original version of the release in the release bundle file. + +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 -V 1.2.3 + +### Arguments + +#### BUNDLE + +path to a release bundle file, e.g. "release.tar" + +### Options + +#### -f, --fleet FLEET + +fleet name or slug (preferred) + +#### -V, --override-version OVERRIDE-VERSION + +Imports this release with the specified version instead of the original version. + ## 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..bf47dee0b9 --- /dev/null +++ b/lib/commands/release/export.ts @@ -0,0 +1,108 @@ +/** + * @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'; +import * as semver from 'balena-semver'; +import { ExpectedError } from '../../errors'; + +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 parsedVersion = semver.parse(params.commitOrId.split(':')[1]); + if (parsedVersion == null) { + throw new ExpectedError( + `Release ${params.commitOrId} could not be exported. The version provided is not a valid semantic version.`, + ); + } else { + const rawVersion = + parsedVersion.build.length === 0 + ? parsedVersion.version + : `${parsedVersion.version}+${parsedVersion.build[0]}`; + release = await balena.models.release.get( + { application: fleet, rawVersion }, + { $select: ['id'] }, + ); + } + } else { + release = await balena.models.release.get(params.commitOrId, { + $select: ['id'], + }); + } + + try { + const releaseBundle = await create({ + sdk: balena, + releaseId: release.id, + }); + await fs.writeFile(options.output, releaseBundle); + console.log( + `Release ${params.commitOrId} has been exported to ${options.output}.`, + ); + } catch (error) { + throw new ExpectedError( + `Release ${params.commitOrId} could not be exported. ${error.message}`, + ); + } + } +} diff --git a/lib/commands/release/import.ts b/lib/commands/release/import.ts new file mode 100644 index 0000000000..b4e536aca4 --- /dev/null +++ b/lib/commands/release/import.ts @@ -0,0 +1,97 @@ +/** + * @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'; +import { ExpectedError } from '../../errors'; + +export default class ReleaseImportCmd extends Command { + public static description = stripIndent` + Imports a release from a release bundle file to an application or fleet. + + The --override-version option is used to specify the version to be used instead + of using the original version of the release in the release bundle file. +`; + 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 -V 1.2.3', + ]; + + public static usage = 'release import '; + + public static flags = { + fleet: { ...cf.fleet, exclusive: ['device'] }, + 'override-version': Flags.string({ + description: + 'Imports this release with the specified version instead of the original version.', + char: 'V', + 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 ExpectedError('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) { + throw new ExpectedError( + `Could not apply release bundle ${params.bundle} to fleet ${options.fleet}. ${error.message}`, + ); + } + } +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 88fa40a98c..0d76b46a59 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-74d8be8776f8278e7f934cfd19076cf0679926f0-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-74d8be8776f8278e7f934cfd19076cf0679926f0-1", + "resolved": "https://registry.npmjs.org/@balena/release-bundle/-/release-bundle-0.5.0-build-add-apply-version-override-74d8be8776f8278e7f934cfd19076cf0679926f0-1.tgz", + "integrity": "sha512-VMPQ7XEbF21IMKUIa25hWlcl+EztaOw6V5Z7VT5xZemy4LAr9zT668AO4Lr1KCjLmsBO0sfPtF03h6hnIlzTXw==", + "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,11 +4309,11 @@ } }, "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.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-cleanup": { @@ -4270,6 +4322,11 @@ "integrity": "sha512-HTksao/sZs9nqxKD/vWOR3WxSrQsyJlBPEFFCgq9lMmhRsuQF+2p6hy+7FaCYn6lOeiDc3ywI8jDQ2bz5y6m8w==", "dev": true }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -5434,6 +5491,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 +5735,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 +5839,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.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.45.tgz", + "integrity": "sha512-VZxPKNNhjKmaC1SUYowuXSRSMGyQGmQjvvA1xE4QZ0xce2kLtEhPDS+kqpCPBZYgqblCLQ2DAjSzmgCM5auvhA==", "dependencies": { "undici-types": "~5.26.4" } @@ -10470,9 +10529,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "engines": { "node": ">= 4" } @@ -18342,9 +18401,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..74255c438e 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-74d8be8776f8278e7f934cfd19076cf0679926f0-1", "@oclif/core": "^3.27.0", "@resin.io/valid-email": "^0.1.0", "@sentry/node": "^6.16.1",