mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-03-12 23:44:32 +00:00
262 lines
9.3 KiB
TypeScript
262 lines
9.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2020-2021 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 { expect } from 'chai';
|
|
import { promises as fs } from 'fs';
|
|
import * as process from 'process';
|
|
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);
|
|
|
|
import { BalenaAPIMock } from '../../nock/balena-api-mock';
|
|
|
|
if (process.platform !== 'win32') {
|
|
describe('balena os configure', function () {
|
|
let imagefs: typeof $imagefs;
|
|
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:
|
|
// EPERM: operation not permitted, rename 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>.inprogress' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>'
|
|
// at async Object.rename (node:internal/fs/promises:782:10) {
|
|
imagefs = await import('balena-image-fs');
|
|
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-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 dtJson = JSON.parse(
|
|
await _fs.promises.readFile('/device-type.json', {
|
|
encoding: 'utf8',
|
|
}),
|
|
);
|
|
expect(dtJson).to.have.nested.property(
|
|
'configuration.config.partition',
|
|
12,
|
|
);
|
|
dtJson.configuration.config.partition = 999;
|
|
await _fs.promises.writeFile(
|
|
'/device-type.json',
|
|
JSON.stringify(dtJson),
|
|
);
|
|
|
|
await _fs.promises.writeFile(
|
|
'/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(() => {
|
|
api = new BalenaAPIMock();
|
|
api.expectGetWhoAmI({ optional: true, persist: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
api.done();
|
|
});
|
|
|
|
after(async () => {
|
|
await fs.unlink(tmpDummyPath);
|
|
await fs.unlink(tmpMatchingDtJsonPartitionPath);
|
|
await fs.unlink(tmpNonMatchingDtJsonPartitionPath);
|
|
});
|
|
|
|
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();
|
|
// 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-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(
|
|
tmpMatchingDtJsonPartitionPath,
|
|
12,
|
|
async (_fs) => {
|
|
const dtJson = JSON.parse(
|
|
await _fs.promises.readFile('/device-type.json', {
|
|
encoding: 'utf8',
|
|
}),
|
|
);
|
|
// confirm that the device-type.json mentions the expected partition
|
|
expect(dtJson).to.have.nested.property(
|
|
'configuration.config.partition',
|
|
12,
|
|
);
|
|
return await _fs.promises.readFile('/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');
|
|
});
|
|
|
|
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 dtJson = JSON.parse(
|
|
await _fs.promises.readFile('/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 _fs.promises.readFile('/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();
|
|
// 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 ${tmpDummyPath}`,
|
|
'--device-type raspberrypi3',
|
|
'--version 2.47.0+rev1',
|
|
'--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(' '));
|
|
// 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.flatMap((line) => line.split('\n')).filter((line) => line !== ''),
|
|
).to.deep.equal(
|
|
stripIndent`
|
|
[warn] "${tmpDummyPath}":
|
|
[warn] Found partition table with 1 partitions,
|
|
[warn] but none with a name/label in ['resin-boot', 'flash-boot', 'balena-boot'].
|
|
[warn] Will scan all partitions for contents.
|
|
[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) => {
|
|
return await _fs.promises.readFile('/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', 'raspberrypi3');
|
|
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
|
|
});
|
|
});
|
|
}
|