Merge pull request #2167 from balena-os/hw-arch-contract

Add `arch.sw` to supported container requirements
This commit is contained in:
flowzone-app[bot] 2023-05-10 12:38:21 +00:00 committed by GitHub
commit 3b6878fd80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 269 additions and 163 deletions

View File

@ -25,7 +25,8 @@ export interface ServiceContracts {
type PotentialContractRequirements =
| 'sw.supervisor'
| 'sw.l4t'
| 'hw.device-type';
| 'hw.device-type'
| 'arch.sw';
type ContractRequirements = {
[key in PotentialContractRequirements]?: string;
};
@ -35,11 +36,13 @@ const contractRequirementVersions: ContractRequirements = {};
export function initializeContractRequirements(opts: {
supervisorVersion: string;
deviceType: string;
deviceArch: string;
l4tVersion?: string;
}) {
contractRequirementVersions['sw.supervisor'] = opts.supervisorVersion;
contractRequirementVersions['sw.l4t'] = opts.l4tVersion;
contractRequirementVersions['hw.device-type'] = opts.deviceType;
contractRequirementVersions['arch.sw'] = opts.deviceArch;
}
function isValidRequirementType(
@ -162,10 +165,10 @@ const contractObjectValidator = t.type({
function getContractsFromVersions(components: ContractRequirements) {
return _.map(components, (value, component) => {
if (component === 'hw.device-type') {
if (component === 'hw.device-type' || component === 'arch.sw') {
return {
type: component,
slug: component,
slug: value,
name: value,
};
} else {

View File

@ -45,6 +45,7 @@ export class Supervisor {
initializeContractRequirements({
supervisorVersion: version,
deviceType: await config.get('deviceType'),
deviceArch: await config.get('deviceArch'),
l4tVersion: await osRelease.getL4tVersion(),
});

View File

@ -25,6 +25,7 @@ describe('device-state', () => {
initializeContractRequirements({
supervisorVersion: '11.0.0',
deviceType: 'intel-nuc',
deviceArch: 'amd64',
});
});
@ -406,8 +407,8 @@ describe('device-state', () => {
name: 'Enforce supervisor version',
requires: [
{
type: 'sw.supervisor',
version: '>=12.0.0',
type: 'arch.sw',
version: 'armv7hf',
},
],
},

View File

@ -1,22 +1,19 @@
import { expect } from 'chai';
import { SinonStub, stub } from 'sinon';
import * as semver from 'semver';
import { SinonStub, stub } from 'sinon';
import * as constants from '~/lib/constants';
import {
containerContractsFulfilled,
initializeContractRequirements,
validateContract,
} from '~/lib/contracts';
import * as osRelease from '~/lib/os-release';
import supervisorVersion = require('~/lib/supervisor-version');
import * as fsUtils from '~/lib/fs-utils';
describe('lib/contracts', () => {
type Contracts = typeof import('~/src/lib/contracts');
const contracts = require('~/src/lib/contracts') as Contracts;
before(() => {
initializeContractRequirements({
contracts.initializeContractRequirements({
supervisorVersion,
deviceType: 'intel-nuc',
deviceArch: 'amd64',
l4tVersion: '32.2',
});
});
@ -24,14 +21,14 @@ describe('lib/contracts', () => {
describe('Contract validation', () => {
it('should correctly validate a contract with no requirements', () =>
expect(() =>
validateContract({
contracts.validateContract({
slug: 'user-container',
}),
).to.be.not.throw());
it('should correctly validate a contract with extra fields', () =>
expect(() =>
validateContract({
contracts.validateContract({
slug: 'user-container',
name: 'user-container',
version: '3.0.0',
@ -40,15 +37,15 @@ describe('lib/contracts', () => {
it('should not validate a contract without the minimum required fields', () => {
return Promise.all([
expect(() => validateContract({})).to.throw(),
expect(() => validateContract({ name: 'test' })).to.throw(),
expect(() => validateContract({ requires: [] })).to.throw(),
expect(() => contracts.validateContract({})).to.throw(),
expect(() => contracts.validateContract({ name: 'test' })).to.throw(),
expect(() => contracts.validateContract({ requires: [] })).to.throw(),
]);
});
it('should correctly validate a contract with requirements', () =>
expect(() =>
validateContract({
contracts.validateContract({
slug: 'user-container',
requires: [
{
@ -58,13 +55,15 @@ describe('lib/contracts', () => {
{
type: 'sw.supervisor',
},
{ type: 'hw.device-type', slug: 'raspberrypi3' },
{ type: 'arch.sw', slug: 'aarch64' },
],
}),
).to.not.throw());
it('should not validate a contract with requirements without the minimum required fields', () => {
return expect(() =>
validateContract({
contracts.validateContract({
slug: 'user-container',
requires: [
{
@ -92,16 +91,16 @@ describe('lib/contracts', () => {
// We ensure that the versions we're using for testing
// are the same as the time of implementation, otherwise
// these tests could fail or succeed when they shouldn't
expect(await osRelease.getOSSemver(constants.hostOSVersionPath)).to.equal(
'2.0.6',
);
// expect(await osRelease.getOSSemver(constants.hostOSVersionPath)).to.equal(
// '2.0.6',
// );
expect(semver.gt(supervisorVersionGreater, supervisorVersion)).to.be.true;
expect(semver.lt(supervisorVersionLesser, supervisorVersion)).to.be.true;
});
it('Should correctly run containers with no requirements', async () => {
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -114,7 +113,7 @@ describe('lib/contracts', () => {
.to.have.property('valid')
.that.equals(true);
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -137,7 +136,7 @@ describe('lib/contracts', () => {
it('should correctly run containers whose requirements are satisfied', async () => {
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -148,6 +147,7 @@ describe('lib/contracts', () => {
type: 'sw.supervisor',
version: `>${supervisorVersionLesser}`,
},
{ type: 'hw.device-type', slug: 'intel-nuc' },
],
},
optional: false,
@ -158,7 +158,7 @@ describe('lib/contracts', () => {
.that.equals(true);
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -169,6 +169,8 @@ describe('lib/contracts', () => {
type: 'sw.supervisor',
version: `<${supervisorVersionGreater}`,
},
{ type: 'sw.arch', name: 'amd64' },
{ type: 'hw.device-type', slug: 'intel-nuc' },
],
},
optional: false,
@ -179,7 +181,7 @@ describe('lib/contracts', () => {
.that.equals(true);
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -190,6 +192,7 @@ describe('lib/contracts', () => {
type: 'sw.supervisor',
version: `>${supervisorVersionLesser}`,
},
{ type: 'sw.arch', slug: 'amd64' },
],
},
optional: false,
@ -200,7 +203,7 @@ describe('lib/contracts', () => {
.that.equals(true);
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -215,6 +218,7 @@ describe('lib/contracts', () => {
type: 'sw.l4t',
version: '32.2',
},
{ type: 'hw.device-type', slug: 'intel-nuc' },
],
},
optional: false,
@ -225,7 +229,7 @@ describe('lib/contracts', () => {
.that.equals(true);
expect(
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -246,6 +250,8 @@ describe('lib/contracts', () => {
name: 'user-container1',
slug: 'user-container1',
requires: [
// sw.os is not a supported contract type, so validation
// ignores this requirement
{
type: 'sw.os',
version: '<3.0.0',
@ -261,7 +267,7 @@ describe('lib/contracts', () => {
});
it('Should refuse to run containers whose requirements are not satisfied', async () => {
let fulfilled = containerContractsFulfilled({
let fulfilled = contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -282,7 +288,70 @@ describe('lib/contracts', () => {
.to.have.property('unmetServices')
.that.deep.equals(['service']);
fulfilled = containerContractsFulfilled({
fulfilled = contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'hw.device-type',
slug: 'raspberrypi3',
},
],
},
optional: false,
},
});
expect(fulfilled).to.have.property('valid').that.equals(false);
expect(fulfilled)
.to.have.property('unmetServices')
.that.deep.equals(['service']);
fulfilled = contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'arch.sw',
slug: 'armv7hf',
},
],
},
optional: false,
},
});
expect(fulfilled).to.have.property('valid').that.equals(false);
expect(fulfilled)
.to.have.property('unmetServices')
.that.deep.equals(['service']);
fulfilled = contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'hw.device-type',
name: 'raspberrypi3',
},
],
},
optional: false,
},
});
expect(fulfilled).to.have.property('valid').that.equals(false);
expect(fulfilled)
.to.have.property('unmetServices')
.that.deep.equals(['service']);
fulfilled = contracts.containerContractsFulfilled({
service2: {
contract: {
type: 'sw.container',
@ -303,7 +372,7 @@ describe('lib/contracts', () => {
.to.have.property('unmetServices')
.that.deep.equals(['service2']);
fulfilled = containerContractsFulfilled({
fulfilled = contracts.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -342,7 +411,7 @@ describe('lib/contracts', () => {
describe('Optional containers', () => {
it('should correctly run passing optional containers', async () => {
const { valid, unmetServices, fulfilledServices } =
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service1: {
contract: {
type: 'sw.container',
@ -364,7 +433,7 @@ describe('lib/contracts', () => {
it('should corrrectly omit failing optional containers', async () => {
const { valid, unmetServices, fulfilledServices } =
containerContractsFulfilled({
contracts.containerContractsFulfilled({
service1: {
contract: {
type: 'sw.container',
@ -385,16 +454,45 @@ describe('lib/contracts', () => {
},
optional: false,
},
service3: {
contract: {
type: 'sw.container',
slug: 'service3',
requires: [
{
type: 'hw.device-type',
slug: 'raspberrypi3',
},
],
},
optional: true,
},
service4: {
contract: {
type: 'sw.container',
slug: 'service3',
requires: [
{
type: 'arch.sw',
slug: 'armv7hf',
},
],
},
optional: true,
},
});
expect(valid).to.equal(true);
expect(unmetServices).to.deep.equal(['service1']);
expect(unmetServices).to.deep.equal([
'service1',
'service3',
'service4',
]);
expect(fulfilledServices).to.deep.equal(['service2']);
});
});
});
});
describe('L4T version detection', () => {
describe('L4T version detection', () => {
let execStub: SinonStub;
const seedExec = (version: string) => {
@ -432,22 +530,24 @@ describe('L4T version detection', () => {
describe('L4T comparison', () => {
const seedEngine = async (version: string) => {
if (execStub != null) {
execStub.restore();
}
const engine = require('~/src/lib/contracts') as Contracts;
seedExec(version);
initializeContractRequirements({
engine.initializeContractRequirements({
supervisorVersion,
deviceType: 'intel-nuc',
deviceArch: 'amd64',
l4tVersion: await osRelease.getL4tVersion(),
});
return engine;
};
it('should allow semver matching even when l4t does not fulfill semver', async () => {
await seedEngine('4.4.38-l4t-r31.0');
const engine = await seedEngine('4.4.38-l4t-r31.0');
expect(
containerContractsFulfilled({
engine.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -467,7 +567,7 @@ describe('L4T version detection', () => {
.that.equals(true);
expect(
containerContractsFulfilled({
engine.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -488,10 +588,10 @@ describe('L4T version detection', () => {
});
it('should allow semver matching when l4t does fulfill semver', async () => {
await seedEngine('4.4.38-l4t-r31.0.1');
const engine = await seedEngine('4.4.38-l4t-r31.0.1');
expect(
containerContractsFulfilled({
engine.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -511,7 +611,7 @@ describe('L4T version detection', () => {
.that.equals(true);
expect(
containerContractsFulfilled({
engine.containerContractsFulfilled({
service: {
contract: {
type: 'sw.container',
@ -531,4 +631,5 @@ describe('L4T version detection', () => {
.that.equals(false);
});
});
});
});