Add contract resolution code, which checks release requirements

Change-type: minor
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2019-08-23 10:16:36 +01:00
parent 2d168784b2
commit 5ce8ba8acf
6 changed files with 1344 additions and 28 deletions

1087
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@
"node": "^6.13.1"
},
"devDependencies": {
"@balena/contrato": "^0.2.1",
"@types/bluebird": "^3.5.25",
"@types/chai": "^4.1.7",
"@types/common-tags": "^1.8.0",

66
src/lib/contracts.ts Normal file
View File

@ -0,0 +1,66 @@
import * as _ from 'lodash';
import { Blueprint, Contract, ContractObject } from '@balena/contrato';
import constants = require('./constants');
import { InternalInconsistencyError } from './errors';
import * as osRelease from './os-release';
import supervisorVersion = require('./supervisor-version');
export async function containerContractsFulfilled(
containers: ContractObject[],
): Promise<boolean> {
const osContract = new Contract({
slug: 'balenaOS',
type: 'sw.os',
name: 'balenaOS',
version: await osRelease.getOSSemver(constants.hostOSVersionPath),
});
const supervisorContract = new Contract({
slug: 'balena-supervisor',
type: 'sw.supervisor',
name: 'balena-supervisor',
version: supervisorVersion,
});
const blueprint = new Blueprint(
{
'sw.os': 1,
'sw.supervisor': 1,
'sw.container': '1+',
},
{
type: 'sw.runnable.configuration',
slug: '{{children.sw.container.slug}}',
},
);
const universe = new Contract({
type: 'meta.universe',
});
universe.addChildren(
[osContract, supervisorContract].concat(
containers.map(c => new Contract(c)),
),
);
const solution = blueprint.reproduce(universe);
if (solution.length > 1) {
throw new InternalInconsistencyError(
'More than one solution available for container contracts when only one is expected!',
);
}
if (solution.length === 0) {
return false;
}
// Detect how many containers are present in the resulting
// solution
const children = solution[0].getChildren({
types: new Set(['sw.container']),
});
return children.length === containers.length;
}

212
test/24-contracts.ts Normal file
View File

@ -0,0 +1,212 @@
import { assert, expect } from 'chai';
import * as semver from 'balena-semver';
import * as constants from '../src/lib/constants';
import { containerContractsFulfilled } from '../src/lib/contracts';
import * as osRelease from '../src/lib/os-release';
import supervisorVersion = require('../src/lib/supervisor-version');
describe('Container contracts', () => {
// Because the supervisor version will change whenever the
// package.json will, we generate values which are above
// and below the current value, and use these to reason
// about the contract engine results
const supervisorVersionGreater = `${semver.major(supervisorVersion)! +
1}.0.0`;
const supervisorVersionLesser = `${semver.major(supervisorVersion)! - 1}.0.0`;
before(async () => {
// 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',
);
assert(semver.gt(supervisorVersionGreater, supervisorVersion));
assert(semver.lt(supervisorVersionLesser, supervisorVersion));
});
it('Should correctly run containers with no requirements', async () => {
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
},
]),
).to.equal(true);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container1',
},
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container2',
},
]),
).to.equal(true);
});
it('should correctly run containers whose requirements are satisfied', async () => {
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'sw.os',
version: '>2.0.0',
},
],
},
]),
).to.equal(true);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'sw.supervisor',
version: `<${supervisorVersionGreater}`,
},
],
},
]),
).to.equal(true);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'sw.supervisor',
version: `>${supervisorVersionLesser}`,
},
],
},
]),
).to.equal(true);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'sw.supervisor',
version: `>${supervisorVersionLesser}`,
},
{
type: 'sw.os',
version: '<3.0.0',
},
],
},
]),
).to.equal(true);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container1',
slug: 'user-container1',
requires: [
{
type: 'sw.supervisor',
version: `>${supervisorVersionLesser}`,
},
],
},
{
type: 'sw.container',
name: 'user-container1',
slug: 'user-container1',
requires: [
{
type: 'sw.os',
version: '<3.0.0',
},
],
},
]),
).to.equal(true);
});
it('Should refuse to run containers whose requirements are not satisfied', async () => {
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container',
slug: 'user-container',
requires: [
{
type: 'sw.os',
version: '>=3.0.0',
},
],
},
]),
).to.equal(false);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container2',
slug: 'user-container2',
requires: [
{
type: 'sw.supervisor',
version: `>=${supervisorVersionLesser}`,
},
{
type: 'sw.os',
version: '>3.0.0',
},
],
},
]),
).to.equal(false);
expect(
await containerContractsFulfilled([
{
type: 'sw.container',
name: 'user-container1',
slug: 'user-container1',
requires: [
{
type: 'sw.supervisor',
version: `>=${supervisorVersionLesser}`,
},
],
},
{
type: 'sw.container',
name: 'user-container2',
slug: 'user-container2',
requires: [
{
type: 'sw.supervisor',
version: `<=${supervisorVersionLesser}`,
},
],
},
]),
).to.equal(false);
});
});

View File

@ -1,2 +1,3 @@
PRETTY_NAME="Resin OS 2.0.6 (fake)"
PRETTY_NAME="balenaOS 2.0.6 (fake)"
VARIANT_ID="dev"
VERSION="2.0.6"

View File

@ -1 +1,2 @@
PRETTY_NAME="Resin OS 2.0.6 (fake)"
PRETTY_NAME="balenaOS 2.0.6 (fake)"
VERSION="2.0.6"