From dbe177e76639040c5fac49746fbbcee2e3360d0a Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Mon, 30 Dec 2024 15:59:54 +0200 Subject: [PATCH] os configure: Give precedence to the boot partition located in the image over the device-type.json contents Update balena-device-init from 8.0.0 to 8.1.0 Change-type: minor --- npm-shrinkwrap.json | 10 ++- package.json | 2 +- src/utils/helpers.ts | 21 ++++++ tests/commands/os/configure.spec.ts | 112 ++++++++++++++++++++++++++-- 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ef72b060c..807d4175e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -17,7 +17,7 @@ "@oclif/core": "^4.1.0", "@sentry/node": "^6.16.1", "balena-config-json": "^4.2.0", - "balena-device-init": "^8.0.0", + "balena-device-init": "^8.1.0", "balena-errors": "^4.7.3", "balena-image-fs": "^7.0.6", "balena-preload": "^16.0.0", @@ -7063,10 +7063,12 @@ } }, "node_modules/balena-device-init": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/balena-device-init/-/balena-device-init-8.0.0.tgz", - "integrity": "sha512-Kaitk9LA8oQsM1suwqYbVAeJrbmSRM4BbzsYJbczItulxRS6XadPWZeN3OLYEV5eUW2Es2SPdqQqkFvIBZriKg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/balena-device-init/-/balena-device-init-8.1.0.tgz", + "integrity": "sha512-nOtrzTcLHhn8uDvZxRRGRGcH9Ry1IoQA86wFB9qK9xEii29f6sl+bti9fOvnDyPCZ1n3pLFFSStE8yN0fNKJQQ==", + "license": "Apache-2.0", "dependencies": { + "balena-config-json": "^4.2.0", "balena-image-fs": "^7.0.6", "balena-semver": "^2.2.0", "lodash": "^4.17.15", diff --git a/package.json b/package.json index 78c441e67..6aa3ddcef 100644 --- a/package.json +++ b/package.json @@ -196,7 +196,7 @@ "@oclif/core": "^4.1.0", "@sentry/node": "^6.16.1", "balena-config-json": "^4.2.0", - "balena-device-init": "^8.0.0", + "balena-device-init": "^8.1.0", "balena-errors": "^4.7.3", "balena-image-fs": "^7.0.6", "balena-preload": "^16.0.0", diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 80b94efa5..f98d08f89 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -110,6 +110,27 @@ export async function getManifest( const init = await import('balena-device-init'); const sdk = getBalenaSdk(); const manifest = await init.getImageManifest(image); + if (manifest != null) { + const config = manifest.configuration?.config; + if (config?.partition != null) { + const { getBootPartition } = await import('balena-config-json'); + // Find the device-type.json property that holds the boot partition number for + // this device type (config.partition or config.partition.primary) and overwrite it + // with the boot partition number that was found by inspecting the image. + // since it's deprecated & no longer updated for newer releases. + if (typeof config.partition === 'number') { + config.partition = await getBootPartition(image); + } else if (config.partition.primary != null) { + config.partition.primary = await getBootPartition(image); + } + // TODO: Add handling for when we no longer include a `config.partition` at all. + } + } else { + // TODO: Change this in the next major to throw, after confirming that this works for all supported OS versions. + console.error( + `[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`, + ); + } if ( manifest != null && manifest.slug !== deviceType && diff --git a/tests/commands/os/configure.spec.ts b/tests/commands/os/configure.spec.ts index 2b3eb2057..3d356ba1a 100644 --- a/tests/commands/os/configure.spec.ts +++ b/tests/commands/os/configure.spec.ts @@ -22,6 +22,7 @@ import { runCommand } from '../../helpers'; import { promisify } from 'util'; import * as tmp from 'tmp'; import type * as $imagefs from 'balena-image-fs'; +import * as stripIndent from 'common-tags/lib/stripIndent'; tmp.setGracefulCleanup(); const tmpNameAsync = promisify(tmp.tmpName); @@ -34,6 +35,7 @@ if (process.platform !== 'win32') { let api: BalenaAPIMock; let tmpDummyPath: string; let tmpMatchingDtJsonPartitionPath: string; + let tmpNonMatchingDtJsonPartitionPath: string; before(async function () { // We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with: @@ -47,6 +49,46 @@ if (process.platform !== 'win32') { './tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img', tmpMatchingDtJsonPartitionPath, ); + + tmpNonMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string; + // Create an image with a device-type.json that mentions a non matching boot partition. + // We copy the pre-existing image and modify it, since including a separate one + // would add 18MB more to the repository. + await fs.copyFile( + './tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img', + tmpNonMatchingDtJsonPartitionPath, + ); + await imagefs.interact( + tmpNonMatchingDtJsonPartitionPath, + 12, + async (_fs) => { + const readFileAsync = promisify(_fs.readFile); + const writeFileAsync = promisify(_fs.writeFile); + + const dtJson = JSON.parse( + await readFileAsync('/device-type.json', { encoding: 'utf8' }), + ); + expect(dtJson).to.have.nested.property( + 'configuration.config.partition', + 12, + ); + dtJson.configuration.config.partition = 999; + await writeFileAsync('/device-type.json', JSON.stringify(dtJson)); + + await writeFileAsync( + '/os-release', + stripIndent` + ID="balena-os" + NAME="balenaOS" + VERSION="6.1.25" + VERSION_ID="6.1.25" + PRETTY_NAME="balenaOS 6.1.25" + DISTRO_CODENAME="kirkstone" + MACHINE="jetson-nano" + META_BALENA_VERSION="6.1.25"`, + ); + }, + ); }); beforeEach(() => { @@ -61,20 +103,20 @@ if (process.platform !== 'win32') { after(async () => { await fs.unlink(tmpDummyPath); await fs.unlink(tmpMatchingDtJsonPartitionPath); + await fs.unlink(tmpNonMatchingDtJsonPartitionPath); }); - it('should inject a valid config.json file to an image with partition 12 as boot & matching device-type.json ', async () => { + it('should detect the OS version and inject a valid config.json file to a 6.0.13 image with partition 12 as boot & matching device-type.json', async () => { api.expectGetApplication(); api.expectGetDeviceTypes(); - // TODO: this shouldn't be necessary & the CLI should be able to find + // It should not reach to /config or /device-types/v1 but instead find // everything required from the device-type.json in the image. - api.expectGetConfigDeviceTypes(); + // api.expectGetConfigDeviceTypes(); api.expectDownloadConfig(); const command: string[] = [ `os configure ${tmpMatchingDtJsonPartitionPath}`, '--device-type jetson-nano', - '--version 6.0.13', '--fleet testApp', '--config-app-update-poll-interval 10', '--config-network ethernet', @@ -111,6 +153,53 @@ if (process.platform !== 'win32') { expect(configObj).to.have.property('initialDeviceName', 'testDeviceName'); }); + it('should detect the OS version and inject a valid config.json file to a 6.1.25 image with partition 12 as boot & a non-matching device-type.json', async () => { + api.expectGetApplication(); + api.expectGetDeviceTypes(); + // It should not reach to /config or /device-types/v1 but instead find + // everything required from the device-type.json in the image. + // api.expectGetConfigDeviceTypes(); + api.expectDownloadConfig(); + + const command: string[] = [ + `os configure ${tmpNonMatchingDtJsonPartitionPath}`, + '--device-type jetson-nano', + '--fleet testApp', + '--config-app-update-poll-interval 10', + '--config-network ethernet', + '--initial-device-name testDeviceName', + '--provisioning-key-name testKey', + '--provisioning-key-expiry-date 2050-12-12', + ]; + + const { err } = await runCommand(command.join(' ')); + expect(err.join('')).to.equal(''); + + // confirm the image contains a config.json... + const config = await imagefs.interact( + tmpNonMatchingDtJsonPartitionPath, + 12, + async (_fs) => { + const readFileAsync = promisify(_fs.readFile); + const dtJson = JSON.parse( + await readFileAsync('/device-type.json', { encoding: 'utf8' }), + ); + // confirm that the device-type.json mentions the expected partition + expect(dtJson).to.have.nested.property( + 'configuration.config.partition', + 999, + ); + return await readFileAsync('/config.json'); + }, + ); + expect(config).to.not.be.empty; + + // confirm the image has the correct config.json values... + const configObj = JSON.parse(config.toString('utf8')); + expect(configObj).to.have.property('deviceType', 'jetson-nano'); + expect(configObj).to.have.property('initialDeviceName', 'testDeviceName'); + }); + // TODO: In the next major consider just failing when we can't find a device-types.json in the image. it('should inject a valid config.json file to a dummy image', async () => { api.expectGetApplication(); @@ -134,7 +223,20 @@ if (process.platform !== 'win32') { const { err } = await runCommand(command.join(' ')); // Once we replace the dummy.img with one that includes a os-release & device-type.json // then we should be able to change this to expect no errors. - expect(err.join('')).to.equal(''); + expect( + err.flatMap((line) => line.split('\n')).filter((line) => line !== ''), + ).to.deep.equal( + stripIndent` + [warn] "${tmpDummyPath}": + [warn] 1 partition(s) found, but none containing file "/device-type.json". + [warn] Assuming default boot partition number '1'. + [warn] "${tmpDummyPath}": + [warn] Could not find a previous "/config.json" file in partition '1'. + [warn] Proceeding anyway, but this is unexpected. + [warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`.split( + '\n', + ), + ); // confirm the image contains a config.json... const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {