Skip to content

Commit

Permalink
os configure: Given the found boot partition precedence over the devi…
Browse files Browse the repository at this point in the history
…ce-type.json contents

Change-type: minor

sq

Resolves: #
Change-type:

sq tests

Resolves: #
Change-type:
  • Loading branch information
thgreasi committed Dec 24, 2024
1 parent 74adeff commit 08d2d3c
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 27 deletions.
18 changes: 18 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ 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');
// Overwrite the deprecated & no longer updated partition number on the device-type.json
// properties, with the boot partition number that we found by inspecting the image.
if (typeof config.partition === 'number') {
config.partition = await getBootPartition(image);
} else if (config.partition.primary != null) {
config.partition.primary = await getBootPartition(image);
}
}
} 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 &&
Expand Down
173 changes: 162 additions & 11 deletions tests/commands/os/configure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { runCommand } from '../../helpers';
import { promisify } from 'util';
import * as tmp from 'tmp';
import * as stripIndent from 'common-tags/lib/stripIndent';
import * as imagefs from 'balena-image-fs';

tmp.setGracefulCleanup();
const tmpNameAsync = promisify(tmp.tmpName);
Expand All @@ -31,27 +32,175 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
if (process.platform !== 'win32') {
describe('balena os configure', function () {
let api: BalenaAPIMock;
let tmpPath: string;
let tmpDummyPath: string;
let tmpMatchingDtJsonPartitionPath: string;
let tmpNonMatchingDtJsonPartitionPath: string;

beforeEach(async () => {
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
tmpPath = (await tmpNameAsync()) as string;
await fs.copyFile('./tests/test-data/dummy.img', tmpPath);
tmpDummyPath = (await tmpNameAsync()) as string;
await fs.copyFile('./tests/test-data/dummy.img', tmpDummyPath);
tmpMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
await fs.copyFile(
'./tests/test-data/mock-jetson-xavier-6.0.23.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-xavier-6.0.23.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.primary',
12,
);
dtJson.configuration.config.partition.primary = 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-xavier"
META_BALENA_VERSION="6.1.25"`,
);
},
);
});

afterEach(async () => {
api.done();
await fs.unlink(tmpPath);
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 () => {
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 ${tmpMatchingDtJsonPartitionPath}`,
'--device-type jetson-xavier',
'--version 6.0.13',
'--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(
tmpMatchingDtJsonPartitionPath,
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.primary',
12,
);
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-xavier');
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
});

it('should inject a valid config.json file to an 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-xavier',
'--version 6.1.25',
'--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.primary',
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-xavier');
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
});

it('should inject a valid config.json file', async () => {
// 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();
// Since the dummy image doesn't include a device-type.json
// we have to reach to the API to fetch the manifest of the device type.
api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();

const command: string[] = [
`os configure ${tmpPath}`,
`os configure ${tmpDummyPath}`,
'--device-type raspberrypi3',
'--version 2.47.0+rev1',
'--fleet testApp',
Expand All @@ -69,17 +218,19 @@ if (process.platform !== 'win32') {
err.flatMap((line) => line.split('\n')).filter((line) => line !== ''),
).to.deep.equal(
stripIndent`
[warn] "${tmpPath}":
[warn] "${tmpDummyPath}":
[warn] 1 partition(s) found, but none containing file "/device-type.json".
[warn] Assuming default boot partition number '1'.
[warn] "${tmpPath}":
[warn] "${tmpDummyPath}":
[warn] Could not find a previous "/config.json" file in partition '1'.
[warn] Proceeding anyway, but this is unexpected.`.split('\n'),
[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 imagefs = await import('balena-image-fs');
const config = await imagefs.interact(tmpPath, 1, async (_fs) => {
const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {
return await promisify(_fs.readFile)('/config.json');
});
expect(config).to.not.be.empty;
Expand Down
35 changes: 19 additions & 16 deletions tests/nock/balena-api-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,25 @@ export class BalenaAPIMock extends NockMock {
}

public expectDownloadConfig(opts: ScopeOpts = {}) {
this.optPost('/download-config', opts).reply(
200,
JSON.parse(`{
"applicationId":1301645,
"deviceType":"raspberrypi3",
"userId":43699,
"appUpdatePollInterval":600000,
"listenPort":48484,
"vpnPort":443,
"apiEndpoint":"https://api.balena-cloud.com",
"vpnEndpoint":"vpn.balena-cloud.com",
"registryEndpoint":"registry2.balena-cloud.com",
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`),
);
this.optPost('/download-config', opts).reply(200, (_uri, body) => {
let deviceType = 'raspberrypi3';
if (typeof body === 'object' && 'deviceType' in body) {
deviceType = body.deviceType;
}
return JSON.parse(`{
"applicationId":1301645,
"deviceType":"${deviceType}",
"userId":43699,
"appUpdatePollInterval":600000,
"listenPort":48484,
"vpnPort":443,
"apiEndpoint":"https://api.balena-cloud.com",
"vpnEndpoint":"vpn.balena-cloud.com",
"registryEndpoint":"registry2.balena-cloud.com",
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`);
});
}

public expectApplicationProvisioning(opts: ScopeOpts = {}) {
Expand Down
Binary file not shown.

0 comments on commit 08d2d3c

Please sign in to comment.