mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-26 05:49:42 +00:00
159 lines
3.6 KiB
TypeScript
159 lines
3.6 KiB
TypeScript
import { isLeft } from 'fp-ts/lib/Either';
|
|
import * as t from 'io-ts';
|
|
import { reporter } from 'io-ts-reporters';
|
|
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 { ContractObject };
|
|
|
|
export interface ServiceContracts {
|
|
[serviceName: string]: { contract?: ContractObject; optional: boolean };
|
|
}
|
|
|
|
export async function containerContractsFulfilled(
|
|
serviceContracts: ServiceContracts,
|
|
): Promise<{
|
|
valid: boolean;
|
|
unmetServices: string[];
|
|
fulfilledServices: string[];
|
|
unmetAndOptional: string[];
|
|
}> {
|
|
const containers = _(serviceContracts)
|
|
.map('contract')
|
|
.compact()
|
|
.value();
|
|
|
|
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,
|
|
...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 {
|
|
valid: false,
|
|
unmetServices: _.keys(serviceContracts),
|
|
fulfilledServices: [],
|
|
unmetAndOptional: [],
|
|
};
|
|
}
|
|
|
|
// Detect how many containers are present in the resulting
|
|
// solution
|
|
const children = solution[0].getChildren({
|
|
types: new Set(['sw.container']),
|
|
});
|
|
|
|
if (children.length === containers.length) {
|
|
return {
|
|
valid: true,
|
|
unmetServices: [],
|
|
fulfilledServices: _.keys(serviceContracts),
|
|
unmetAndOptional: [],
|
|
};
|
|
} else {
|
|
// If we got here, it means that at least one of the
|
|
// container contracts was not fulfilled. If *all* of
|
|
// those containers whose contract was not met are
|
|
// marked as optional, the target state is still valid,
|
|
// but we ignore the optional containers
|
|
|
|
const [fulfilledServices, unfulfilledServices] = _.partition(
|
|
_.keys(serviceContracts),
|
|
serviceName => {
|
|
const { contract } = serviceContracts[serviceName];
|
|
if (!contract) {
|
|
return true;
|
|
}
|
|
// Did we find the contract in the generated state?
|
|
return _.some(children, child =>
|
|
_.isEqual((child as any).raw, contract),
|
|
);
|
|
},
|
|
);
|
|
|
|
const [unmetAndRequired, unmetAndOptional] = _.partition(
|
|
unfulfilledServices,
|
|
serviceName => {
|
|
return !serviceContracts[serviceName].optional;
|
|
},
|
|
);
|
|
|
|
return {
|
|
valid: unmetAndRequired.length === 0,
|
|
unmetServices: unfulfilledServices,
|
|
fulfilledServices,
|
|
unmetAndOptional,
|
|
};
|
|
}
|
|
}
|
|
|
|
const contractObjectValidator = t.type({
|
|
slug: t.string,
|
|
requires: t.union([
|
|
t.null,
|
|
t.undefined,
|
|
t.array(
|
|
t.type({
|
|
type: t.string,
|
|
version: t.union([t.null, t.undefined, t.string]),
|
|
}),
|
|
),
|
|
]),
|
|
});
|
|
|
|
export function validateContract(
|
|
contract: unknown,
|
|
): contract is ContractObject {
|
|
const result = contractObjectValidator.decode(contract);
|
|
|
|
if (isLeft(result)) {
|
|
throw new Error(reporter(result).join('\n'));
|
|
}
|
|
|
|
return true;
|
|
}
|