mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-18 21:27:54 +00:00
Refactor contracts validation code
This updates the interfaces on lib/contracts and the validation in the application-manager module.
This commit is contained in:
parent
e9f460fd75
commit
48e526ec43
@ -9,12 +9,11 @@ import * as deviceConfig from '../device-config';
|
||||
import * as eventTracker from '../event-tracker';
|
||||
import { loadBackupFromMigration } from '../lib/migration';
|
||||
|
||||
import { InternalInconsistencyError, TargetStateError } from '../lib/errors';
|
||||
import {
|
||||
ContractValidationError,
|
||||
ContractViolationError,
|
||||
InternalInconsistencyError,
|
||||
TargetStateError,
|
||||
} from '../lib/errors';
|
||||
} from '../lib/contracts';
|
||||
|
||||
import log from '../lib/supervisor-console';
|
||||
|
||||
|
@ -4,18 +4,14 @@ import type StrictEventEmitter from 'strict-event-emitter-types';
|
||||
|
||||
import * as config from '../config';
|
||||
import type { Transaction } from '../db';
|
||||
import { transaction } from '../db';
|
||||
import * as logger from '../logger';
|
||||
import LocalModeManager from '../local-mode';
|
||||
|
||||
import * as dbFormat from '../device-state/db-format';
|
||||
import { validateTargetContracts } from '../lib/contracts';
|
||||
import * as contracts from '../lib/contracts';
|
||||
import * as constants from '../lib/constants';
|
||||
import log from '../lib/supervisor-console';
|
||||
import {
|
||||
ContractViolationError,
|
||||
InternalInconsistencyError,
|
||||
} from '../lib/errors';
|
||||
import { InternalInconsistencyError } from '../lib/errors';
|
||||
import { getServicesLockedByAppId, LocksTakenMap } from '../lib/update-lock';
|
||||
import { checkTruthy } from '../lib/validation';
|
||||
|
||||
@ -503,14 +499,14 @@ export async function executeStep(
|
||||
export async function setTarget(
|
||||
apps: TargetApps,
|
||||
source: string,
|
||||
maybeTrx?: Transaction,
|
||||
trx: Transaction,
|
||||
) {
|
||||
const setInTransaction = async (
|
||||
$filteredApps: TargetApps,
|
||||
trx: Transaction,
|
||||
$trx: Transaction,
|
||||
) => {
|
||||
await dbFormat.setApps($filteredApps, source, trx);
|
||||
await trx('app')
|
||||
await dbFormat.setApps($filteredApps, source, $trx);
|
||||
await $trx('app')
|
||||
.where({ source })
|
||||
.whereNotIn(
|
||||
'appId',
|
||||
@ -534,49 +530,43 @@ export async function setTarget(
|
||||
// useless - The exception to this rule is when the only
|
||||
// failing services are marked as optional, then we
|
||||
// filter those out and add the target state to the database
|
||||
const contractViolators: { [appName: string]: string[] } = {};
|
||||
const fulfilledContracts = validateTargetContracts(apps);
|
||||
const contractViolators: contracts.ContractViolators = {};
|
||||
const fulfilledContracts = contracts.validateTargetContracts(apps);
|
||||
const filteredApps = structuredClone(apps);
|
||||
_.each(
|
||||
fulfilledContracts,
|
||||
(
|
||||
{ valid, unmetServices, fulfilledServices, unmetAndOptional },
|
||||
appUuid,
|
||||
) => {
|
||||
if (!valid) {
|
||||
contractViolators[apps[appUuid].name] = unmetServices;
|
||||
return delete filteredApps[appUuid];
|
||||
} else {
|
||||
// valid is true, but we could still be missing
|
||||
// some optional containers, and need to filter
|
||||
// these out of the target state
|
||||
const [releaseUuid] = Object.keys(filteredApps[appUuid].releases);
|
||||
if (releaseUuid) {
|
||||
const services =
|
||||
filteredApps[appUuid].releases[releaseUuid].services ?? {};
|
||||
filteredApps[appUuid].releases[releaseUuid].services = _.pick(
|
||||
services,
|
||||
Object.keys(services).filter((serviceName) =>
|
||||
fulfilledServices.includes(serviceName),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const [
|
||||
appUuid,
|
||||
{ valid, unmetServices, unmetAndOptional },
|
||||
] of Object.entries(fulfilledContracts)) {
|
||||
if (!valid) {
|
||||
contractViolators[appUuid] = {
|
||||
appId: apps[appUuid].id,
|
||||
appName: apps[appUuid].name,
|
||||
services: unmetServices.map(({ serviceName }) => serviceName),
|
||||
};
|
||||
|
||||
if (unmetAndOptional.length !== 0) {
|
||||
return reportOptionalContainers(unmetAndOptional);
|
||||
}
|
||||
// Remove the invalid app from the list
|
||||
delete filteredApps[appUuid];
|
||||
} else {
|
||||
// App is valid, but we could still be missing
|
||||
// some optional containers, and need to filter
|
||||
// these out of the target state
|
||||
const app = filteredApps[appUuid];
|
||||
for (const { commit, serviceName } of unmetAndOptional) {
|
||||
delete app.releases[commit].services[serviceName];
|
||||
}
|
||||
},
|
||||
);
|
||||
let promise;
|
||||
if (maybeTrx != null) {
|
||||
promise = setInTransaction(filteredApps, maybeTrx);
|
||||
} else {
|
||||
promise = transaction((trx) => setInTransaction(filteredApps, trx));
|
||||
|
||||
if (unmetAndOptional.length !== 0) {
|
||||
reportOptionalContainers(
|
||||
unmetAndOptional.map(({ serviceName }) => serviceName),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await promise;
|
||||
|
||||
await setInTransaction(filteredApps, trx);
|
||||
if (!_.isEmpty(contractViolators)) {
|
||||
throw new ContractViolationError(contractViolators);
|
||||
// TODO: add rejected state for contract violator apps
|
||||
throw new contracts.ContractViolationError(contractViolators);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,23 +2,66 @@ import { isLeft } from 'fp-ts/lib/Either';
|
||||
import * as t from 'io-ts';
|
||||
import Reporter from 'io-ts-reporters';
|
||||
import _ from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import type { ContractObject } from '@balena/contrato';
|
||||
import { Blueprint, Contract } from '@balena/contrato';
|
||||
|
||||
import { ContractValidationError, InternalInconsistencyError } from './errors';
|
||||
import { InternalInconsistencyError } from './errors';
|
||||
import { checkTruthy } from './validation';
|
||||
import type { TargetApps } from '../types';
|
||||
|
||||
export interface ApplicationContractResult {
|
||||
valid: boolean;
|
||||
unmetServices: string[];
|
||||
fulfilledServices: string[];
|
||||
unmetAndOptional: string[];
|
||||
/**
|
||||
* This error is thrown when a container contract does not
|
||||
* match the minimum we expect from it
|
||||
*/
|
||||
export class ContractValidationError extends TypedError {
|
||||
constructor(serviceName: string, error: string) {
|
||||
super(
|
||||
`The contract for service ${serviceName} failed validation, with error: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceContracts {
|
||||
[serviceName: string]: { contract?: ContractObject; optional: boolean };
|
||||
export interface ContractViolators {
|
||||
[appUuid: string]: { appName: string; appId: number; services: string[] };
|
||||
}
|
||||
|
||||
/**
|
||||
* This error is thrown when one or releases cannot be ran
|
||||
* as one or more of their container have unmet requirements.
|
||||
* It accepts a map of app names to arrays of service names
|
||||
* which have unmet requirements.
|
||||
*/
|
||||
export class ContractViolationError extends TypedError {
|
||||
constructor(violators: ContractViolators) {
|
||||
const appStrings = Object.values(violators).map(
|
||||
({ appName, services }) =>
|
||||
`${appName}: Services with unmet requirements: ${services.join(', ')}`,
|
||||
);
|
||||
super(
|
||||
`Some releases were rejected due to having unmet requirements:\n ${appStrings.join(
|
||||
'\n ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceCtx {
|
||||
serviceName: string;
|
||||
commit: string;
|
||||
}
|
||||
|
||||
export interface AppContractResult {
|
||||
valid: boolean;
|
||||
unmetServices: ServiceCtx[];
|
||||
fulfilledServices: ServiceCtx[];
|
||||
unmetAndOptional: ServiceCtx[];
|
||||
}
|
||||
|
||||
interface ServiceWithContract extends ServiceCtx {
|
||||
contract?: ContractObject;
|
||||
optional: boolean;
|
||||
}
|
||||
|
||||
type PotentialContractRequirements =
|
||||
@ -52,12 +95,15 @@ function isValidRequirementType(
|
||||
}
|
||||
|
||||
export function containerContractsFulfilled(
|
||||
serviceContracts: ServiceContracts,
|
||||
): ApplicationContractResult {
|
||||
const containers = _(serviceContracts).map('contract').compact().value();
|
||||
servicesWithContract: ServiceWithContract[],
|
||||
): AppContractResult {
|
||||
const containers = servicesWithContract
|
||||
.map(({ contract }) => contract)
|
||||
.filter((c) => c != null) satisfies ContractObject[];
|
||||
const contractTypes = Object.keys(contractRequirementVersions);
|
||||
|
||||
const blueprintMembership: Dictionary<number> = {};
|
||||
for (const component of _.keys(contractRequirementVersions)) {
|
||||
for (const component of contractTypes) {
|
||||
blueprintMembership[component] = 1;
|
||||
}
|
||||
const blueprint = new Blueprint(
|
||||
@ -89,10 +135,11 @@ export function containerContractsFulfilled(
|
||||
'More than one solution available for container contracts when only one is expected!',
|
||||
);
|
||||
}
|
||||
|
||||
if (solution.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
unmetServices: _.keys(serviceContracts),
|
||||
unmetServices: servicesWithContract,
|
||||
fulfilledServices: [],
|
||||
unmetAndOptional: [],
|
||||
};
|
||||
@ -108,7 +155,7 @@ export function containerContractsFulfilled(
|
||||
return {
|
||||
valid: true,
|
||||
unmetServices: [],
|
||||
fulfilledServices: _.keys(serviceContracts),
|
||||
fulfilledServices: servicesWithContract,
|
||||
unmetAndOptional: [],
|
||||
};
|
||||
} else {
|
||||
@ -117,16 +164,14 @@ export function containerContractsFulfilled(
|
||||
// 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];
|
||||
servicesWithContract,
|
||||
({ contract }) => {
|
||||
if (!contract) {
|
||||
return true;
|
||||
}
|
||||
// Did we find the contract in the generated state?
|
||||
return _.some(children, (child) =>
|
||||
return children.some((child) =>
|
||||
_.isEqual((child as any).raw, contract),
|
||||
);
|
||||
},
|
||||
@ -134,9 +179,7 @@ export function containerContractsFulfilled(
|
||||
|
||||
const [unmetAndRequired, unmetAndOptional] = _.partition(
|
||||
unfulfilledServices,
|
||||
(serviceName) => {
|
||||
return !serviceContracts[serviceName].optional;
|
||||
},
|
||||
({ optional }) => !optional,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -198,67 +241,43 @@ export function validateContract(contract: unknown): boolean {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateTargetContracts(
|
||||
apps: TargetApps,
|
||||
): Dictionary<ApplicationContractResult> {
|
||||
return Object.keys(apps)
|
||||
.map((appUuid): [string, ApplicationContractResult] => {
|
||||
const app = apps[appUuid];
|
||||
const [release] = Object.values(app.releases);
|
||||
const serviceContracts = Object.keys(release?.services ?? [])
|
||||
.map((serviceName) => {
|
||||
const service = release.services[serviceName];
|
||||
const { contract } = service;
|
||||
if (contract) {
|
||||
try {
|
||||
// Validate the contract syntax
|
||||
validateContract(contract);
|
||||
): Dictionary<AppContractResult> {
|
||||
const result: Dictionary<AppContractResult> = {};
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
contract,
|
||||
optional: checkTruthy(
|
||||
service.labels?.['io.balena.features.optional'],
|
||||
),
|
||||
};
|
||||
} catch (e: any) {
|
||||
throw new ContractValidationError(serviceName, e.message);
|
||||
}
|
||||
for (const [appUuid, app] of Object.entries(apps)) {
|
||||
const releases = Object.entries(app.releases);
|
||||
if (releases.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// While app.releases is an object, we expect a target to only
|
||||
// contain a single release per app so we use just the first element
|
||||
const [commit, release] = releases[0];
|
||||
|
||||
const servicesWithContract = Object.entries(release.services ?? {}).map(
|
||||
([serviceName, { contract, labels = {} }]) => {
|
||||
if (contract) {
|
||||
try {
|
||||
validateContract(contract);
|
||||
} catch (e: any) {
|
||||
throw new ContractValidationError(serviceName, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a default contract for the service if no contract is defined
|
||||
return { serviceName, contract: undefined, optional: false };
|
||||
})
|
||||
// map by serviceName
|
||||
.reduce(
|
||||
(contracts, { serviceName, ...serviceContract }) => ({
|
||||
...contracts,
|
||||
[serviceName]: serviceContract,
|
||||
}),
|
||||
{} as ServiceContracts,
|
||||
);
|
||||
|
||||
if (Object.keys(serviceContracts).length > 0) {
|
||||
// Validate service contracts if any
|
||||
return [appUuid, containerContractsFulfilled(serviceContracts)];
|
||||
}
|
||||
|
||||
// Return success if no services are found
|
||||
return [
|
||||
appUuid,
|
||||
{
|
||||
valid: true,
|
||||
fulfilledServices: Object.keys(release?.services ?? []),
|
||||
unmetAndOptional: [],
|
||||
unmetServices: [],
|
||||
},
|
||||
];
|
||||
})
|
||||
.reduce(
|
||||
(result, [appUuid, contractFulfilled]) => ({
|
||||
...result,
|
||||
[appUuid]: contractFulfilled,
|
||||
}),
|
||||
{} as Dictionary<ApplicationContractResult>,
|
||||
return {
|
||||
serviceName,
|
||||
commit,
|
||||
contract,
|
||||
optional: checkTruthy(labels['io.balena.features.optional']),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
result[appUuid] = containerContractsFulfilled(servicesWithContract);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { endsWith, map } from 'lodash';
|
||||
import { endsWith } from 'lodash';
|
||||
import { TypedError } from 'typed-error';
|
||||
|
||||
import { checkInt } from './validation';
|
||||
@ -104,39 +104,6 @@ export class TargetStateError extends TypedError {}
|
||||
*/
|
||||
export class SupervisorContainerNotFoundError extends TypedError {}
|
||||
|
||||
/**
|
||||
* This error is thrown when a container contract does not
|
||||
* match the minimum we expect from it
|
||||
*/
|
||||
export class ContractValidationError extends TypedError {
|
||||
constructor(serviceName: string, error: string) {
|
||||
super(
|
||||
`The contract for service ${serviceName} failed validation, with error: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This error is thrown when one or releases cannot be ran
|
||||
* as one or more of their container have unmet requirements.
|
||||
* It accepts a map of app names to arrays of service names
|
||||
* which have unmet requirements.
|
||||
*/
|
||||
export class ContractViolationError extends TypedError {
|
||||
constructor(violators: { [appName: string]: string[] }) {
|
||||
const appStrings = map(
|
||||
violators,
|
||||
(svcs, name) =>
|
||||
`${name}: Services with unmet requirements: ${svcs.join(', ')}`,
|
||||
);
|
||||
super(
|
||||
`Some releases were rejected due to having unmet requirements:\n ${appStrings.join(
|
||||
'\n ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AppsJsonParseError extends TypedError {}
|
||||
export class DatabaseParseError extends TypedError {}
|
||||
export class BackupError extends TypedError {}
|
||||
|
@ -1,84 +0,0 @@
|
||||
import * as constants from '~/lib/constants';
|
||||
import * as db from '~/src/db';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
// Creates a test database and returns a query builder
|
||||
export async function createDB() {
|
||||
const oldDatabasePath = process.env.DATABASE_PATH;
|
||||
|
||||
// for testing we use an in memory database
|
||||
process.env.DATABASE_PATH = ':memory:';
|
||||
|
||||
// @ts-expect-error need to rewrite the value of databasePath as that
|
||||
// is used directly by the db module
|
||||
constants.databasePath = process.env.DATABASE_PATH;
|
||||
|
||||
// Cleanup the module cache in order to have it reloaded in the local context
|
||||
delete require.cache[require.resolve('~/src/db')];
|
||||
|
||||
// Initialize the database module
|
||||
await db.initialized();
|
||||
|
||||
// Get the knex instance to allow queries to the db
|
||||
const { models, upsertModel } = db;
|
||||
|
||||
// This is hacky but haven't found another way to do it,
|
||||
// stubbing the db methods here ensures the module under test
|
||||
// is using the database we want
|
||||
sinon.stub(db, 'models').callsFake(models);
|
||||
sinon.stub(db, 'upsertModel').callsFake(upsertModel);
|
||||
|
||||
return {
|
||||
// Returns a query builder instance for the given
|
||||
// table in order perform data operations
|
||||
models,
|
||||
|
||||
// Resets the database to initial value post
|
||||
// migrations
|
||||
async reset() {
|
||||
// Reset the contents of the db
|
||||
await db.transaction(async (trx: any) => {
|
||||
const result = await trx.raw(`
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
WHERE type='table'`);
|
||||
for (const r of result) {
|
||||
// We don't run the migrations again
|
||||
if (r.name !== 'knex_migrations') {
|
||||
await trx.raw(`DELETE FROM ${r.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The supervisor expects this value to already have
|
||||
// been pre-populated
|
||||
await trx('deviceConfig').insert({ targetValues: '{}' });
|
||||
});
|
||||
|
||||
// Reset stub call history
|
||||
(db.models as sinon.SinonStub).resetHistory();
|
||||
(db.upsertModel as sinon.SinonStub).resetHistory();
|
||||
},
|
||||
|
||||
// Destroys the in-memory database and resets environment
|
||||
async destroy() {
|
||||
// Remove data from the in memory database just in case
|
||||
await this.reset();
|
||||
|
||||
// Restore the old datbase path
|
||||
process.env.DATABASE_PATH = oldDatabasePath;
|
||||
|
||||
// Restore stubs
|
||||
(db.models as sinon.SinonStub).restore();
|
||||
(db.upsertModel as sinon.SinonStub).restore();
|
||||
|
||||
// @ts-expect-error restore the constant default
|
||||
constants.databasePath = process.env.DATABASE_PATH;
|
||||
|
||||
// Cleanup the module cache in order to have it reloaded
|
||||
// correctly next time it's used
|
||||
delete require.cache[require.resolve('~/src/db')];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type TestDatabase = UnwrappedPromise<ReturnType<typeof createDB>>;
|
@ -101,35 +101,41 @@ describe('lib/contracts', () => {
|
||||
|
||||
it('Should correctly run containers with no requirements', async () => {
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container1',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
service2: {
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service2',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container2',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
@ -137,8 +143,10 @@ describe('lib/contracts', () => {
|
||||
|
||||
it('should correctly run containers whose requirements are satisfied', async () => {
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -153,14 +161,16 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -176,14 +186,16 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -198,14 +210,16 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -224,14 +238,16 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
|
||||
expect(
|
||||
contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container1',
|
||||
@ -245,7 +261,9 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
service2: {
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service2',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container1',
|
||||
@ -261,15 +279,17 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
});
|
||||
|
||||
it('Should refuse to run containers whose requirements are not satisfied', async () => {
|
||||
let fulfilled = contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
it('should refuse to run containers whose requirements are not satisfied', async () => {
|
||||
let fulfilled = contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -283,14 +303,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(fulfilled).to.have.property('valid').that.equals(false);
|
||||
expect(fulfilled)
|
||||
.to.have.property('unmetServices')
|
||||
.that.deep.equals(['service']);
|
||||
expect(fulfilled).to.have.property('unmetServices').with.lengthOf(1);
|
||||
expect(fulfilled.unmetServices[0]).to.deep.include({
|
||||
serviceName: 'service',
|
||||
commit: 'd0',
|
||||
});
|
||||
|
||||
fulfilled = contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
fulfilled = contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -304,14 +328,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(fulfilled).to.have.property('valid').that.equals(false);
|
||||
expect(fulfilled)
|
||||
.to.have.property('unmetServices')
|
||||
.that.deep.equals(['service']);
|
||||
expect(fulfilled).to.have.property('unmetServices').with.lengthOf(1);
|
||||
expect(fulfilled.unmetServices[0]).to.deep.include({
|
||||
serviceName: 'service',
|
||||
commit: 'd0',
|
||||
});
|
||||
|
||||
fulfilled = contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
fulfilled = contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -325,14 +353,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(fulfilled).to.have.property('valid').that.equals(false);
|
||||
expect(fulfilled)
|
||||
.to.have.property('unmetServices')
|
||||
.that.deep.equals(['service']);
|
||||
expect(fulfilled).to.have.property('unmetServices').with.lengthOf(1);
|
||||
expect(fulfilled.unmetServices[0]).to.deep.include({
|
||||
serviceName: 'service',
|
||||
commit: 'd0',
|
||||
});
|
||||
|
||||
fulfilled = contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
fulfilled = contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container',
|
||||
@ -346,14 +378,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(fulfilled).to.have.property('valid').that.equals(false);
|
||||
expect(fulfilled)
|
||||
.to.have.property('unmetServices')
|
||||
.that.deep.equals(['service']);
|
||||
expect(fulfilled).to.have.property('unmetServices').with.lengthOf(1);
|
||||
expect(fulfilled.unmetServices[0]).to.deep.include({
|
||||
serviceName: 'service',
|
||||
commit: 'd0',
|
||||
});
|
||||
|
||||
fulfilled = contracts.containerContractsFulfilled({
|
||||
service2: {
|
||||
fulfilled = contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service2',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container2',
|
||||
@ -367,14 +403,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(fulfilled).to.have.property('valid').that.equals(false);
|
||||
expect(fulfilled)
|
||||
.to.have.property('unmetServices')
|
||||
.that.deep.equals(['service2']);
|
||||
expect(fulfilled).to.have.property('unmetServices').with.lengthOf(1);
|
||||
expect(fulfilled.unmetServices[0]).to.deep.include({
|
||||
serviceName: 'service2',
|
||||
commit: 'd0',
|
||||
});
|
||||
|
||||
fulfilled = contracts.containerContractsFulfilled({
|
||||
service: {
|
||||
fulfilled = contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container1',
|
||||
@ -388,7 +428,9 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
service2: {
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service2',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
name: 'user-container2',
|
||||
@ -402,18 +444,22 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(fulfilled).to.have.property('valid').that.equals(false);
|
||||
expect(fulfilled)
|
||||
.to.have.property('unmetServices')
|
||||
.that.deep.equals(['service2']);
|
||||
expect(fulfilled).to.have.property('unmetServices').with.lengthOf(1);
|
||||
expect(fulfilled.unmetServices[0]).to.deep.include({
|
||||
serviceName: 'service2',
|
||||
commit: 'd0',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Optional containers', () => {
|
||||
it('should correctly run passing optional containers', async () => {
|
||||
const { valid, unmetServices, fulfilledServices } =
|
||||
contracts.containerContractsFulfilled({
|
||||
service1: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service1',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'service1',
|
||||
@ -426,16 +472,22 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
]);
|
||||
|
||||
expect(valid).to.equal(true);
|
||||
expect(unmetServices).to.deep.equal([]);
|
||||
expect(fulfilledServices).to.deep.equal(['service1']);
|
||||
expect(fulfilledServices[0]).to.deep.include({
|
||||
serviceName: 'service1',
|
||||
commit: 'd0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should corrrectly omit failing optional containers', async () => {
|
||||
const { valid, unmetServices, fulfilledServices } =
|
||||
contracts.containerContractsFulfilled({
|
||||
service1: {
|
||||
contracts.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service1',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'service1',
|
||||
@ -448,14 +500,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
service2: {
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service2',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'service2',
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
service3: {
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service3',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'service3',
|
||||
@ -468,10 +524,12 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
service4: {
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service4',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'service3',
|
||||
slug: 'service4',
|
||||
requires: [
|
||||
{
|
||||
type: 'arch.sw',
|
||||
@ -481,14 +539,18 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
]);
|
||||
expect(valid).to.equal(true);
|
||||
expect(unmetServices).to.deep.equal([
|
||||
expect(unmetServices.map((s) => s.serviceName)).to.deep.equal([
|
||||
'service1',
|
||||
'service3',
|
||||
'service4',
|
||||
]);
|
||||
expect(fulfilledServices).to.deep.equal(['service2']);
|
||||
expect(fulfilledServices).to.have.lengthOf(1);
|
||||
expect(fulfilledServices[0]).to.deep.include({
|
||||
serviceName: 'service2',
|
||||
commit: 'd0',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -548,8 +610,10 @@ describe('lib/contracts', () => {
|
||||
const engine = await seedEngine('4.4.38-l4t-r31.0');
|
||||
|
||||
expect(
|
||||
engine.containerContractsFulfilled({
|
||||
service: {
|
||||
engine.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container',
|
||||
@ -562,14 +626,16 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
|
||||
expect(
|
||||
engine.containerContractsFulfilled({
|
||||
service: {
|
||||
engine.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container',
|
||||
@ -582,7 +648,7 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(false);
|
||||
@ -592,8 +658,10 @@ describe('lib/contracts', () => {
|
||||
const engine = await seedEngine('4.4.38-l4t-r31.0.1');
|
||||
|
||||
expect(
|
||||
engine.containerContractsFulfilled({
|
||||
service: {
|
||||
engine.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container',
|
||||
@ -606,14 +674,16 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(true);
|
||||
|
||||
expect(
|
||||
engine.containerContractsFulfilled({
|
||||
service: {
|
||||
engine.containerContractsFulfilled([
|
||||
{
|
||||
commit: 'd0',
|
||||
serviceName: 'service',
|
||||
contract: {
|
||||
type: 'sw.container',
|
||||
slug: 'user-container',
|
||||
@ -626,7 +696,7 @@ describe('lib/contracts', () => {
|
||||
},
|
||||
optional: false,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.to.have.property('valid')
|
||||
.that.equals(false);
|
||||
|
Loading…
Reference in New Issue
Block a user