mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-20 16:20:16 +00:00
Add support for container contracts
These contracts can specify an OS version and supervisor version that they require. If any of the containers in a release have requirements that are not met, the release is rejected, and the previous release continues to run. Change-type: minor Closes: #1086 Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@ -4511,12 +4511,14 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@ -4536,7 +4538,8 @@
|
|||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@ -4684,6 +4687,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import { EventTracker } from './event-tracker';
|
|||||||
|
|
||||||
import * as constants from './lib/constants';
|
import * as constants from './lib/constants';
|
||||||
import {
|
import {
|
||||||
|
ContractValidationError,
|
||||||
|
ContractViolationError,
|
||||||
DuplicateUuidError,
|
DuplicateUuidError,
|
||||||
ExchangeKeyError,
|
ExchangeKeyError,
|
||||||
InternalInconsistencyError,
|
InternalInconsistencyError,
|
||||||
@ -28,6 +30,7 @@ import { DeviceApplicationState } from './types/state';
|
|||||||
import log from './lib/supervisor-console';
|
import log from './lib/supervisor-console';
|
||||||
|
|
||||||
import DeviceState = require('./device-state');
|
import DeviceState = require('./device-state');
|
||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
const REPORT_SUCCESS_DELAY = 1000;
|
const REPORT_SUCCESS_DELAY = 1000;
|
||||||
const MAX_REPORT_RETRY_DELAY = 60000;
|
const MAX_REPORT_RETRY_DELAY = 60000;
|
||||||
@ -44,6 +47,7 @@ export interface APIBinderConstructOpts {
|
|||||||
db: Database;
|
db: Database;
|
||||||
deviceState: DeviceState;
|
deviceState: DeviceState;
|
||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
@ -74,6 +78,7 @@ export class APIBinder {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
private eventTracker: EventTracker;
|
private eventTracker: EventTracker;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
public balenaApi: PinejsClientRequest | null = null;
|
public balenaApi: PinejsClientRequest | null = null;
|
||||||
private cachedBalenaApi: PinejsClientRequest | null = null;
|
private cachedBalenaApi: PinejsClientRequest | null = null;
|
||||||
@ -96,10 +101,12 @@ export class APIBinder {
|
|||||||
config,
|
config,
|
||||||
deviceState,
|
deviceState,
|
||||||
eventTracker,
|
eventTracker,
|
||||||
|
logger,
|
||||||
}: APIBinderConstructOpts) {
|
}: APIBinderConstructOpts) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.deviceState = deviceState;
|
this.deviceState = deviceState;
|
||||||
this.eventTracker = eventTracker;
|
this.eventTracker = eventTracker;
|
||||||
|
this.logger = logger;
|
||||||
|
|
||||||
this.router = this.createAPIBinderRouter(this);
|
this.router = this.createAPIBinderRouter(this);
|
||||||
}
|
}
|
||||||
@ -580,9 +587,25 @@ export class APIBinder {
|
|||||||
this.deviceState.triggerApplyTarget({ force, isFromApi });
|
this.deviceState.triggerApplyTarget({ force, isFromApi });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.tapCatch(err => {
|
.tapCatch(ContractValidationError, ContractViolationError, e => {
|
||||||
log.error(`Failed to get target state for device: ${err}`);
|
log.error(`Could not store target state for device: ${e}`);
|
||||||
|
this.logger.logSystemMessage(
|
||||||
|
`Could not move to new release: ${e.message}`,
|
||||||
|
{},
|
||||||
|
'targetStateRejection',
|
||||||
|
false,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
|
.tapCatch(
|
||||||
|
(e: unknown) =>
|
||||||
|
!(
|
||||||
|
e instanceof ContractValidationError ||
|
||||||
|
e instanceof ContractViolationError
|
||||||
|
),
|
||||||
|
err => {
|
||||||
|
log.error(`Failed to get target state for device: ${err}`);
|
||||||
|
},
|
||||||
|
)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.lastTargetStateFetch = process.hrtime();
|
this.lastTargetStateFetch = process.hrtime();
|
||||||
});
|
});
|
||||||
|
@ -9,11 +9,12 @@ path = require 'path'
|
|||||||
constants = require './lib/constants'
|
constants = require './lib/constants'
|
||||||
{ log } = require './lib/supervisor-console'
|
{ log } = require './lib/supervisor-console'
|
||||||
|
|
||||||
|
{ containerContractsFulfilled, validateContract } = require './lib/contracts'
|
||||||
{ DockerUtils: Docker } = require './lib/docker-utils'
|
{ DockerUtils: Docker } = require './lib/docker-utils'
|
||||||
{ LocalModeManager } = require './local-mode'
|
{ LocalModeManager } = require './local-mode'
|
||||||
updateLock = require './lib/update-lock'
|
updateLock = require './lib/update-lock'
|
||||||
{ checkTruthy, checkInt, checkString } = require './lib/validation'
|
{ checkTruthy, checkInt, checkString } = require './lib/validation'
|
||||||
{ NotFoundError } = require './lib/errors'
|
{ ContractViolationError, ContractValidationError, NotFoundError } = require './lib/errors'
|
||||||
{ pathExistsOnHost } = require './lib/fs-utils'
|
{ pathExistsOnHost } = require './lib/fs-utils'
|
||||||
|
|
||||||
{ ApplicationTargetStateWrapper } = require './target-state'
|
{ ApplicationTargetStateWrapper } = require './target-state'
|
||||||
@ -683,9 +684,31 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
)
|
)
|
||||||
|
|
||||||
setTarget: (apps, dependent , source, trx) =>
|
setTarget: (apps, dependent , source, trx) =>
|
||||||
setInTransaction = (trx) =>
|
# We look at the container contracts here, as if we
|
||||||
|
# cannot run the release, we don't want it to be added
|
||||||
|
# to the database, overwriting the current release. This
|
||||||
|
# is because if we just reject the release, but leave it
|
||||||
|
# in the db, if for any reason the current state stops
|
||||||
|
# running, we won't restart it, leaving the device useless
|
||||||
|
contractsFulfilled = _.mapValues apps, (app) ->
|
||||||
|
serviceContracts = {}
|
||||||
|
_.each app.services, (s) ->
|
||||||
|
if s.contract?
|
||||||
|
try
|
||||||
|
validateContract(s)
|
||||||
|
catch e
|
||||||
|
throw new ContractValidationError(s.serviceName, e.message)
|
||||||
|
serviceContracts[s.serviceName] = s.contract
|
||||||
|
|
||||||
|
if !_.isEmpty(serviceContracts)
|
||||||
|
containerContractsFulfilled(serviceContracts)
|
||||||
|
else
|
||||||
|
{ valid: true }
|
||||||
|
|
||||||
|
|
||||||
|
setInTransaction = (filteredApps, trx) =>
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
appsArray = _.map apps, (app, appId) ->
|
appsArray = _.map filteredApps, (app, appId) ->
|
||||||
appClone = _.clone(app)
|
appClone = _.clone(app)
|
||||||
appClone.appId = checkInt(appId)
|
appClone.appId = checkInt(appId)
|
||||||
appClone.source = source
|
appClone.source = source
|
||||||
@ -694,17 +717,34 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
.tap (appsForDB) =>
|
.tap (appsForDB) =>
|
||||||
@targetStateWrapper.setTargetApps(appsForDB, trx)
|
@targetStateWrapper.setTargetApps(appsForDB, trx)
|
||||||
.then (appsForDB) ->
|
.then (appsForDB) ->
|
||||||
trx('app').where({ source }).whereNotIn('appId', _.map(appsForDB, 'appId')).del()
|
trx('app').where({ source }).whereNotIn('appId',
|
||||||
|
# Use apps here, rather than filteredApps, to
|
||||||
|
# avoid removing a release from the database
|
||||||
|
# without an application to replace it.
|
||||||
|
# Currently this will only happen if the release
|
||||||
|
# which would replace it fails a contract
|
||||||
|
# validation check
|
||||||
|
_.map(apps, (_, appId) -> checkInt(appId))
|
||||||
|
).del()
|
||||||
.then =>
|
.then =>
|
||||||
@proxyvisor.setTargetInTransaction(dependent, trx)
|
@proxyvisor.setTargetInTransaction(dependent, trx)
|
||||||
|
|
||||||
Promise.try =>
|
contractViolators = {}
|
||||||
|
Promise.props(contractsFulfilled).then (fulfilledContracts) ->
|
||||||
|
filteredApps = _.cloneDeep(apps)
|
||||||
|
_.each fulfilledContracts, ({ valid, unmetServices }, appId) ->
|
||||||
|
if not valid
|
||||||
|
contractViolators[apps[appId].name] = unmetServices
|
||||||
|
delete filteredApps[appId]
|
||||||
if trx?
|
if trx?
|
||||||
setInTransaction(trx)
|
setInTransaction(filteredApps, trx)
|
||||||
else
|
else
|
||||||
@db.transaction(setInTransaction)
|
@db.transaction(setInTransaction)
|
||||||
.then =>
|
.then =>
|
||||||
@_targetVolatilePerImageId = {}
|
@_targetVolatilePerImageId = {}
|
||||||
|
.finally ->
|
||||||
|
if not _.isEmpty(contractViolators)
|
||||||
|
throw new ContractViolationError(contractViolators)
|
||||||
|
|
||||||
setTargetVolatileForService: (imageId, target) =>
|
setTargetVolatileForService: (imageId, target) =>
|
||||||
@_targetVolatilePerImageId[imageId] ?= {}
|
@_targetVolatilePerImageId[imageId] ?= {}
|
||||||
@ -933,6 +973,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
conf = { delta, localMode }
|
conf = { delta, localMode }
|
||||||
if conf.localMode
|
if conf.localMode
|
||||||
cleanupNeeded = false
|
cleanupNeeded = false
|
||||||
|
|
||||||
@_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf, containerIds)
|
@_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf, containerIds)
|
||||||
.then (nextSteps) =>
|
.then (nextSteps) =>
|
||||||
if ignoreImages and _.some(nextSteps, action: 'fetch')
|
if ignoreImages and _.some(nextSteps, action: 'fetch')
|
||||||
|
@ -113,6 +113,8 @@ export class Service {
|
|||||||
service.createdAt = appConfig.createdAt;
|
service.createdAt = appConfig.createdAt;
|
||||||
delete appConfig.createdAt;
|
delete appConfig.createdAt;
|
||||||
|
|
||||||
|
delete appConfig.contract;
|
||||||
|
|
||||||
// We don't need this value
|
// We don't need this value
|
||||||
delete appConfig.commit;
|
delete appConfig.commit;
|
||||||
|
|
||||||
|
@ -17,7 +17,12 @@ validation = require './lib/validation'
|
|||||||
systemd = require './lib/systemd'
|
systemd = require './lib/systemd'
|
||||||
updateLock = require './lib/update-lock'
|
updateLock = require './lib/update-lock'
|
||||||
{ singleToMulticontainerApp } = require './lib/migration'
|
{ singleToMulticontainerApp } = require './lib/migration'
|
||||||
{ ENOENT, EISDIR, NotFoundError, UpdatesLockedError } = require './lib/errors'
|
{
|
||||||
|
ENOENT,
|
||||||
|
EISDIR,
|
||||||
|
NotFoundError,
|
||||||
|
UpdatesLockedError
|
||||||
|
} = require './lib/errors'
|
||||||
|
|
||||||
{ DeviceConfig } = require './device-config'
|
{ DeviceConfig } = require './device-config'
|
||||||
ApplicationManager = require './application-manager'
|
ApplicationManager = require './application-manager'
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { isLeft } from 'fp-ts/lib/Either';
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
import { reporter } from 'io-ts-reporters';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { Blueprint, Contract, ContractObject } from '@balena/contrato';
|
import { Blueprint, Contract, ContractObject } from '@balena/contrato';
|
||||||
@ -7,9 +10,17 @@ import { InternalInconsistencyError } from './errors';
|
|||||||
import * as osRelease from './os-release';
|
import * as osRelease from './os-release';
|
||||||
import supervisorVersion = require('./supervisor-version');
|
import supervisorVersion = require('./supervisor-version');
|
||||||
|
|
||||||
|
export { ContractObject };
|
||||||
|
|
||||||
|
export interface ServiceContracts {
|
||||||
|
[serviceName: string]: ContractObject;
|
||||||
|
}
|
||||||
|
|
||||||
export async function containerContractsFulfilled(
|
export async function containerContractsFulfilled(
|
||||||
containers: ContractObject[],
|
serviceContracts: ServiceContracts,
|
||||||
): Promise<boolean> {
|
): Promise<{ valid: boolean; unmetServices: string[] }> {
|
||||||
|
const containers = _.values(serviceContracts);
|
||||||
|
|
||||||
const osContract = new Contract({
|
const osContract = new Contract({
|
||||||
slug: 'balenaOS',
|
slug: 'balenaOS',
|
||||||
type: 'sw.os',
|
type: 'sw.os',
|
||||||
@ -54,7 +65,7 @@ export async function containerContractsFulfilled(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (solution.length === 0) {
|
if (solution.length === 0) {
|
||||||
return false;
|
return { valid: false, unmetServices: _.keys(serviceContracts) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect how many containers are present in the resulting
|
// Detect how many containers are present in the resulting
|
||||||
@ -62,5 +73,50 @@ export async function containerContractsFulfilled(
|
|||||||
const children = solution[0].getChildren({
|
const children = solution[0].getChildren({
|
||||||
types: new Set(['sw.container']),
|
types: new Set(['sw.container']),
|
||||||
});
|
});
|
||||||
return children.length === containers.length;
|
|
||||||
|
if (children.length === containers.length) {
|
||||||
|
return { valid: true, unmetServices: [] };
|
||||||
|
} else {
|
||||||
|
// Work out which service violated the contracts they
|
||||||
|
// provided
|
||||||
|
const unmetServices = _(serviceContracts)
|
||||||
|
.map((contract, serviceName) => {
|
||||||
|
const found = _.find(children, child => {
|
||||||
|
return _.isEqual((child as any).raw, contract);
|
||||||
|
});
|
||||||
|
if (found == null) {
|
||||||
|
return serviceName;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.filter(n => n != null)
|
||||||
|
.value() as string[];
|
||||||
|
return { valid: false, unmetServices };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { endsWith, startsWith } from 'lodash';
|
import { endsWith, map, startsWith } from 'lodash';
|
||||||
import TypedError = require('typed-error');
|
import TypedError = require('typed-error');
|
||||||
|
|
||||||
import { checkInt } from './validation';
|
import { checkInt } from './validation';
|
||||||
@ -68,3 +68,36 @@ export class ImageAuthenticationError extends TypedError {}
|
|||||||
* See LocalModeManager for a usage example.
|
* See LocalModeManager for a usage example.
|
||||||
*/
|
*/
|
||||||
export class SupervisorContainerNotFoundError 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 ',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import fs = require('mz/fs');
|
import fs = require('mz/fs');
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ export class Supervisor {
|
|||||||
db: this.db,
|
db: this.db,
|
||||||
deviceState: this.deviceState,
|
deviceState: this.deviceState,
|
||||||
eventTracker: this.eventTracker,
|
eventTracker: this.eventTracker,
|
||||||
|
logger: this.logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: rearchitect proxyvisor to avoid this circular dependency
|
// FIXME: rearchitect proxyvisor to avoid this circular dependency
|
||||||
|
@ -1,20 +1,87 @@
|
|||||||
import { assert, expect } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
|
|
||||||
import * as semver from 'balena-semver';
|
import * as semver from 'semver';
|
||||||
|
|
||||||
import * as constants from '../src/lib/constants';
|
import * as constants from '../src/lib/constants';
|
||||||
import { containerContractsFulfilled } from '../src/lib/contracts';
|
import {
|
||||||
|
containerContractsFulfilled,
|
||||||
|
validateContract,
|
||||||
|
} from '../src/lib/contracts';
|
||||||
import * as osRelease from '../src/lib/os-release';
|
import * as osRelease from '../src/lib/os-release';
|
||||||
import supervisorVersion = require('../src/lib/supervisor-version');
|
import supervisorVersion = require('../src/lib/supervisor-version');
|
||||||
|
|
||||||
describe('Container contracts', () => {
|
describe('Container contracts', () => {
|
||||||
|
describe('Contract validation', () => {
|
||||||
|
it('should correctly validate a contract with no requirements', () => {
|
||||||
|
assert(
|
||||||
|
validateContract({
|
||||||
|
slug: 'user-container',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly validate a contract with extra fields', () => {
|
||||||
|
assert(
|
||||||
|
validateContract({
|
||||||
|
slug: 'user-container',
|
||||||
|
name: 'user-container',
|
||||||
|
version: '3.0.0',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate a contract without the minimum required fields', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateContract({});
|
||||||
|
}).to.throw();
|
||||||
|
expect(() => {
|
||||||
|
validateContract({ name: 'test' });
|
||||||
|
}).to.throw();
|
||||||
|
expect(() => {
|
||||||
|
validateContract({ requires: [] });
|
||||||
|
}).to.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly validate a contract with requirements', () => {
|
||||||
|
assert(
|
||||||
|
validateContract({
|
||||||
|
slug: 'user-container',
|
||||||
|
requires: [
|
||||||
|
{
|
||||||
|
type: 'sw.os',
|
||||||
|
version: '>3.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'sw.supervisor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate a contract with requirements without the minimum required fields', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateContract({
|
||||||
|
slug: 'user-container',
|
||||||
|
requires: [
|
||||||
|
{
|
||||||
|
version: '>3.0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).to.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Requirement resolution', () => {
|
||||||
// Because the supervisor version will change whenever the
|
// Because the supervisor version will change whenever the
|
||||||
// package.json will, we generate values which are above
|
// package.json will, we generate values which are above
|
||||||
// and below the current value, and use these to reason
|
// and below the current value, and use these to reason
|
||||||
// about the contract engine results
|
// about the contract engine results
|
||||||
const supervisorVersionGreater = `${semver.major(supervisorVersion)! +
|
const supervisorVersionGreater = `${semver.major(supervisorVersion)! +
|
||||||
1}.0.0`;
|
1}.0.0`;
|
||||||
const supervisorVersionLesser = `${semver.major(supervisorVersion)! - 1}.0.0`;
|
const supervisorVersionLesser = `${semver.major(supervisorVersion)! -
|
||||||
|
1}.0.0`;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// We ensure that the versions we're using for testing
|
// We ensure that the versions we're using for testing
|
||||||
@ -29,34 +96,35 @@ describe('Container contracts', () => {
|
|||||||
|
|
||||||
it('Should correctly run containers with no requirements', async () => {
|
it('Should correctly run containers with no requirements', async () => {
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
|
||||||
slug: 'user-container',
|
slug: 'user-container',
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
|
||||||
slug: 'user-container1',
|
slug: 'user-container1',
|
||||||
},
|
},
|
||||||
{
|
service2: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
|
||||||
slug: 'user-container2',
|
slug: 'user-container2',
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly run containers whose requirements are satisfied', async () => {
|
it('should correctly run containers whose requirements are satisfied', async () => {
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
name: 'user-container',
|
||||||
slug: 'user-container',
|
slug: 'user-container',
|
||||||
@ -67,12 +135,14 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
name: 'user-container',
|
||||||
slug: 'user-container',
|
slug: 'user-container',
|
||||||
@ -83,11 +153,14 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
name: 'user-container',
|
||||||
slug: 'user-container',
|
slug: 'user-container',
|
||||||
@ -98,11 +171,14 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
name: 'user-container',
|
||||||
slug: 'user-container',
|
slug: 'user-container',
|
||||||
@ -117,11 +193,13 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
expect(
|
expect(
|
||||||
await containerContractsFulfilled([
|
await containerContractsFulfilled({
|
||||||
{
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container1',
|
name: 'user-container1',
|
||||||
slug: 'user-container1',
|
slug: 'user-container1',
|
||||||
@ -132,7 +210,7 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
service2: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container1',
|
name: 'user-container1',
|
||||||
slug: 'user-container1',
|
slug: 'user-container1',
|
||||||
@ -143,14 +221,15 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
}),
|
||||||
).to.equal(true);
|
)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should refuse to run containers whose requirements are not satisfied', async () => {
|
it('Should refuse to run containers whose requirements are not satisfied', async () => {
|
||||||
expect(
|
let fulfilled = await containerContractsFulfilled({
|
||||||
await containerContractsFulfilled([
|
service: {
|
||||||
{
|
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container',
|
name: 'user-container',
|
||||||
slug: 'user-container',
|
slug: 'user-container',
|
||||||
@ -161,11 +240,16 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
});
|
||||||
).to.equal(false);
|
expect(fulfilled)
|
||||||
expect(
|
.to.have.property('valid')
|
||||||
await containerContractsFulfilled([
|
.that.equals(false);
|
||||||
{
|
expect(fulfilled)
|
||||||
|
.to.have.property('unmetServices')
|
||||||
|
.that.deep.equals(['service']);
|
||||||
|
|
||||||
|
fulfilled = await containerContractsFulfilled({
|
||||||
|
service2: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container2',
|
name: 'user-container2',
|
||||||
slug: 'user-container2',
|
slug: 'user-container2',
|
||||||
@ -180,11 +264,16 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
});
|
||||||
).to.equal(false);
|
expect(fulfilled)
|
||||||
expect(
|
.to.have.property('valid')
|
||||||
await containerContractsFulfilled([
|
.that.equals(false);
|
||||||
{
|
expect(fulfilled)
|
||||||
|
.to.have.property('unmetServices')
|
||||||
|
.that.deep.equals(['service2']);
|
||||||
|
|
||||||
|
fulfilled = await containerContractsFulfilled({
|
||||||
|
service: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container1',
|
name: 'user-container1',
|
||||||
slug: 'user-container1',
|
slug: 'user-container1',
|
||||||
@ -195,7 +284,7 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
service2: {
|
||||||
type: 'sw.container',
|
type: 'sw.container',
|
||||||
name: 'user-container2',
|
name: 'user-container2',
|
||||||
slug: 'user-container2',
|
slug: 'user-container2',
|
||||||
@ -206,7 +295,13 @@ describe('Container contracts', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
});
|
||||||
).to.equal(false);
|
expect(fulfilled)
|
||||||
|
.to.have.property('valid')
|
||||||
|
.that.equals(false);
|
||||||
|
expect(fulfilled)
|
||||||
|
.to.have.property('unmetServices')
|
||||||
|
.that.deep.equals(['service2']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user