diff --git a/package-lock.json b/package-lock.json index 37d22ee9..bb847166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1195,8 +1195,7 @@ "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" }, "array-from": { "version": "2.1.1", @@ -1573,7 +1572,7 @@ "fetch-ponyfill": "^4.0.0", "fetch-readablestream": "^0.2.0", "lodash": "^4.6.1", - "node-web-streams": "github:resin-io-modules/node-web-streams#46f98300b69090bde3f6b4983877ccfe283a892c", + "node-web-streams": "github:resin-io-modules/node-web-streams#emit-errors", "progress-stream": "^2.0.0", "qs": "^6.3.0", "rindle": "^1.3.1" @@ -1594,6 +1593,15 @@ "resolved": "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz", "integrity": "sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw==", "dev": true + }, + "node-web-streams": { + "version": "github:resin-io-modules/node-web-streams#46f98300b69090bde3f6b4983877ccfe283a892c", + "from": "github:resin-io-modules/node-web-streams#emit-errors", + "dev": true, + "requires": { + "is-stream": "^1.1.0", + "web-streams-polyfill": "^1.3.2" + } } } }, @@ -1709,7 +1717,7 @@ "lodash": "^4.13.1", "resin-cli-form": "^1.4.1", "resin-cli-visuals": "^1.2.8", - "resin-discoverable-services": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#afca9e4700ec5ef82aa897f14bd5a46f06518061", + "resin-discoverable-services": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#find-on-all-interfaces", "resin-semver": "^1.4.0", "revalidator": "^0.3.1", "rindle": "^1.3.0", @@ -1727,6 +1735,18 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "bonjour": { + "version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", + "from": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#listen-on-all-interfaces", + "multicast-dns-service-types": "^1.1.0" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -1765,6 +1785,32 @@ "tar-stream": "^1.5.2" } }, + "resin-discoverable-services": { + "version": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#afca9e4700ec5ef82aa897f14bd5a46f06518061", + "from": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#find-on-all-interfaces", + "dev": true, + "requires": { + "bluebird": "^3.0.0", + "bonjour": "git+https://github.com/resin-io/bonjour.git#fixed-mdns", + "ip": "^1.1.4", + "lodash": "^4.17.4" + }, + "dependencies": { + "bonjour": { + "version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", + "from": "git+https://github.com/resin-io/bonjour.git#fixed-mdns", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#listen-on-all-interfaces", + "multicast-dns-service-types": "^1.1.0" + } + } + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -1951,19 +1997,6 @@ } } }, - "bonjour": { - "version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", - "from": "git+https://github.com/resin-io/bonjour.git#fixed-mdns", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc", - "multicast-dns-service-types": "^1.1.0" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2133,8 +2166,7 @@ "buffer-indexof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" }, "buffer-xor": { "version": "1.0.3", @@ -3010,10 +3042,9 @@ } }, "deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", - "integrity": "sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "requires": { "is-arguments": "^1.0.4", "is-date-object": "^1.0.1", @@ -3070,7 +3101,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -3268,14 +3298,12 @@ "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" }, "dns-packet": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", - "dev": true, "requires": { "ip": "^1.1.0", "safe-buffer": "^5.0.1" @@ -3285,7 +3313,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, "requires": { "buffer-indexof": "^1.0.0" } @@ -5185,8 +5212,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "g-status": { "version": "2.0.2", @@ -5565,7 +5591,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -6231,8 +6256,7 @@ "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, "ipaddr.js": { "version": "1.8.0", @@ -6273,8 +6297,7 @@ "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" }, "is-arrayish": { "version": "0.2.1", @@ -6337,8 +6360,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-descriptor": { "version": "0.1.6", @@ -6487,7 +6509,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, "requires": { "has": "^1.0.1" } @@ -6847,7 +6868,7 @@ }, "chalk": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", "dev": true, "requires": { @@ -8227,8 +8248,7 @@ }, "multicast-dns": { "version": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc", - "from": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc", - "dev": true, + "from": "git+https://github.com/resin-io-modules/multicast-dns.git#listen-on-all-interfaces", "requires": { "dns-packet": "^1.0.1", "thunky": "^0.1.0" @@ -8237,8 +8257,7 @@ "multicast-dns-service-types": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, "mute-stream": { "version": "0.0.5", @@ -8481,15 +8500,6 @@ "integrity": "sha512-W0SgGKaB9qSCfFfNj2uQZ/5BlVumaNHjVCAPdEoXrkEJ3ynSf/806LEz1rbDFbJ4+PL9G8IxRkJJTvZndd5D9g==", "dev": true }, - "node-web-streams": { - "version": "github:resin-io-modules/node-web-streams#46f98300b69090bde3f6b4983877ccfe283a892c", - "from": "github:resin-io-modules/node-web-streams#46f98300b69090bde3f6b4983877ccfe283a892c", - "dev": true, - "requires": { - "is-stream": "^1.1.0", - "web-streams-polyfill": "^1.3.2" - } - }, "noop-logger": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", @@ -8632,14 +8642,12 @@ "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -9540,7 +9548,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", - "dev": true, "requires": { "define-properties": "^1.1.2" } @@ -9734,17 +9741,6 @@ "lodash": "^4.0.0" } }, - "resin-discoverable-services": { - "version": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#afca9e4700ec5ef82aa897f14bd5a46f06518061", - "from": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#afca9e4700ec5ef82aa897f14bd5a46f06518061", - "dev": true, - "requires": { - "bluebird": "^3.0.0", - "bonjour": "git+https://github.com/resin-io/bonjour.git#fixed-mdns", - "ip": "^1.1.4", - "lodash": "^4.17.4" - } - }, "resin-lint": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/resin-lint/-/resin-lint-3.1.1.tgz", @@ -9773,9 +9769,9 @@ }, "dependencies": { "@types/bluebird": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.28.tgz", - "integrity": "sha512-0Vk/kqkukxPKSzP9c8WJgisgGDx5oZDbsLLWIP5t70yThO/YleE+GEm2S1GlRALTaack3O7U5OS5qEm7q2kciA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz", + "integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==", "dev": true }, "@types/node": { @@ -9795,6 +9791,12 @@ "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true } } }, @@ -11078,8 +11080,7 @@ "thunky": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz", - "integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=", - "dev": true + "integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=" }, "tildify": { "version": "1.2.0", diff --git a/package.json b/package.json index ff483af7..5fb2f49c 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ "build": "webpack", "build:debug": "npm run typescript:release && npm run coffeescript:release && npm run migrations:copy && npm run packagejson:copy", "lint": "npm run lint:coffee && npm run lint:typescript", - "test": "npm run lint && npm run test:build && JUNIT_REPORT_PATH=report.xml istanbul cover _mocha && npm run coverage", + "test": "npm run lint && npm run test:build && TEST=1 JUNIT_REPORT_PATH=report.xml istanbul cover _mocha && npm run coverage", "test:build": "npm run typescript:test-build && npm run coffeescript:test && npm run testitems:copy && npm run migrations:copy-test && npm run packagejson:copy", "coverage": "istanbul report text && istanbul report html", - "test:fast": "mocha --opts test/fast-mocha.opts", - "test:debug": "npm run test:build && mocha --inspect-brk", + "test:fast": "TEST=1 mocha --opts test/fast-mocha.opts", + "test:debug": "npm run test:build && TEST=1 mocha --inspect-brk", "prettify": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"{src,test,typings}/**/*.ts\"", "typescript:test-build": "tsc --project tsconfig.json", "typescript:release": "tsc --project tsconfig.release.json && cp -r build/src/* build && rm -rf build/src", diff --git a/src/application-manager.coffee b/src/application-manager.coffee index 867f0e30..79a64d05 100644 --- a/src/application-manager.coffee +++ b/src/application-manager.coffee @@ -9,12 +9,12 @@ path = require 'path' constants = require './lib/constants' { log } = require './lib/supervisor-console' -{ containerContractsFulfilled, validateContract } = require './lib/contracts' +{ validateTargetContracts } = require './lib/contracts' { DockerUtils: Docker } = require './lib/docker-utils' { LocalModeManager } = require './local-mode' updateLock = require './lib/update-lock' { checkTruthy, checkInt, checkString } = require './lib/validation' -{ ContractViolationError, ContractValidationError, NotFoundError } = require './lib/errors' +{ ContractViolationError, NotFoundError } = require './lib/errors' { pathExistsOnHost } = require './lib/fs-utils' { TargetStateAccessor } = require './target-state' @@ -683,34 +683,7 @@ module.exports = class ApplicationManager extends EventEmitter return outApp ) - setTarget: (apps, dependent , source, 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.contract) - catch e - throw new ContractValidationError(s.serviceName, e.message) - serviceContracts[s.serviceName] = - contract: s.contract, - optional: checkTruthy(s.labels['io.balena.features.optional']) ? false - else - serviceContracts[s.serviceName] = { contract: null, optional: false } - - if !_.isEmpty(serviceContracts) - containerContractsFulfilled(serviceContracts) - else - { valid: true, fulfilledServices: _.map(app.services, 'serviceName') } - - + setTarget: (apps, dependent , source, trx) -> setInTransaction = (filteredApps, trx) => Promise.try => appsArray = _.map filteredApps, (app, appId) -> @@ -734,8 +707,18 @@ module.exports = class ApplicationManager extends EventEmitter .then => @proxyvisor.setTargetInTransaction(dependent, 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 - 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 contractViolators = {} - Promise.props(contractsFulfilled).then (fulfilledContracts) => + Promise.resolve(validateTargetContracts(apps)) + .then (fulfilledContracts) => filteredApps = _.cloneDeep(apps) _.each( fulfilledContracts, diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts index f262b985..3779ca33 100644 --- a/src/lib/contracts.ts +++ b/src/lib/contracts.ts @@ -5,48 +5,84 @@ import * as _ from 'lodash'; import { Blueprint, Contract, ContractObject } from '@balena/contrato'; -import constants = require('./constants'); -import { InternalInconsistencyError } from './errors'; +import { ContractValidationError, InternalInconsistencyError } from './errors'; import * as osRelease from './os-release'; import supervisorVersion = require('./supervisor-version'); +import { checkTruthy } from './validation'; export { ContractObject }; +// TODO{type}: When target and current state are correctly +// defined, replace this +interface AppWithContracts { + services: { + [key: string]: { + serviceName: string; + contract?: ContractObject; + labels?: Dictionary; + }; + }; +} + +export interface ApplicationContractResult { + valid: boolean; + unmetServices: string[]; + fulfilledServices: string[]; + unmetAndOptional: string[]; +} + export interface ServiceContracts { [serviceName: string]: { contract?: ContractObject; optional: boolean }; } +type PotentialContractRequirements = 'sw.supervisor' | 'sw.l4t'; +type ContractRequirementVersions = { + [key in PotentialContractRequirements]?: string; +}; + +let contractRequirementVersions = async () => { + const versions: ContractRequirementVersions = { + 'sw.supervisor': supervisorVersion, + }; + const l4tVersion = await osRelease.getL4tVersion(); + if (l4tVersion != null) { + versions['sw.l4t'] = l4tVersion; + } + + return versions; +}; + +// When running in tests, we need this function to be +// repeatedly executed, but on-device, this should only be +// executed once per run +if (process.env.TEST !== '1') { + contractRequirementVersions = _.once(contractRequirementVersions); +} + +function isValidRequirementType( + requirementVersions: ContractRequirementVersions, + requirement: string, +) { + return requirement in requirementVersions; +} + export async function containerContractsFulfilled( serviceContracts: ServiceContracts, -): Promise<{ - valid: boolean; - unmetServices: string[]; - fulfilledServices: string[]; - unmetAndOptional: string[]; -}> { +): Promise { 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 versions = await contractRequirementVersions(); + const blueprintMembership: Dictionary = {}; + for (const component of _.keys(versions)) { + blueprintMembership[component] = 1; + } const blueprint = new Blueprint( { - 'sw.os': 1, - 'sw.supervisor': 1, + ...blueprintMembership, 'sw.container': '1+', }, { @@ -59,11 +95,11 @@ export async function containerContractsFulfilled( type: 'meta.universe', }); - universe.addChildren([ - osContract, - supervisorContract, - ...containers.map(c => new Contract(c)), - ]); + universe.addChildren( + [...getContractsFromVersions(versions), ...containers].map( + c => new Contract(c), + ), + ); const solution = blueprint.reproduce(universe); @@ -145,14 +181,76 @@ const contractObjectValidator = t.type({ ]), }); -export function validateContract( - contract: unknown, -): contract is ContractObject { +function getContractsFromVersions(versions: ContractRequirementVersions) { + return _.map(versions, (version, component) => ({ + type: component, + slug: component, + name: component, + version, + })); +} + +export async function validateContract(contract: unknown): Promise { const result = contractObjectValidator.decode(contract); if (isLeft(result)) { throw new Error(reporter(result).join('\n')); } + const requirementVersions = await contractRequirementVersions(); + + for (const { type } of result.right.requires || []) { + if (!isValidRequirementType(requirementVersions, type)) { + throw new Error(`${type} is not a valid contract requirement type`); + } + } + return true; } +export async function validateTargetContracts( + apps: Dictionary, +): Promise> { + const appsFulfilled: Dictionary = {}; + + for (const appId of _.keys(apps)) { + const app = apps[appId]; + const serviceContracts: ServiceContracts = {}; + + for (const svcId of _.keys(app.services)) { + const svc = app.services[svcId]; + + if (svc.contract) { + try { + await validateContract(svc.contract); + + serviceContracts[svc.serviceName] = { + contract: svc.contract, + optional: + checkTruthy(svc.labels?.['io.balena.features.optional']) || false, + }; + } catch (e) { + throw new ContractValidationError(svc.serviceName, e.message); + } + } else { + serviceContracts[svc.serviceName] = { + contract: undefined, + optional: false, + }; + } + + if (!_.isEmpty(serviceContracts)) { + appsFulfilled[appId] = await containerContractsFulfilled( + serviceContracts, + ); + } else { + appsFulfilled[appId] = { + valid: true, + fulfilledServices: _.map(app.services, 'serviceName'), + unmetAndOptional: [], + unmetServices: [], + }; + } + } + } + return appsFulfilled; +} diff --git a/src/lib/os-release.ts b/src/lib/os-release.ts index 295b0c7f..7ed62ef5 100644 --- a/src/lib/os-release.ts +++ b/src/lib/os-release.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import fs = require('mz/fs'); +import { child_process, fs } from 'mz'; import { InternalInconsistencyError } from './errors'; import log from './supervisor-console'; @@ -58,3 +58,20 @@ export function getOSVariant(path: string): Promise { export function getOSSemver(path: string): Promise { return getOSReleaseField(path, 'VERSION'); } + +const L4T_REGEX = /^.*-l4t-r(\d+\.\d+).*$/; +export async function getL4tVersion(): Promise { + // We call `uname -r` on the host, and look for l4t + try { + const [stdout] = await child_process.exec('uname -r'); + const match = L4T_REGEX.exec(stdout.toString().trim()); + if (match == null) { + return; + } + + return match[1]; + } catch (e) { + log.error('Could not detect l4t version! Error: ', e); + return; + } +} diff --git a/test/24-contracts.ts b/test/24-contracts.ts index 23350a4a..6ebce915 100644 --- a/test/24-contracts.ts +++ b/test/24-contracts.ts @@ -1,5 +1,7 @@ import { assert, expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import { child_process } from 'mz'; import * as semver from 'semver'; import * as constants from '../src/lib/constants'; @@ -11,56 +13,63 @@ import * as osRelease from '../src/lib/os-release'; import supervisorVersion = require('../src/lib/supervisor-version'); describe('Container contracts', () => { + let execStub: SinonStub; + before(() => { + execStub = stub(child_process, 'exec').returns( + Promise.resolve([ + Buffer.from('4.9.140-l4t-r32.2+g3dcbed5'), + Buffer.from(''), + ]), + ); + }); + + after(() => { + execStub.restore(); + }); + describe('Contract validation', () => { - it('should correctly validate a contract with no requirements', () => { - assert( + it('should correctly validate a contract with no requirements', () => + expect( validateContract({ slug: 'user-container', }), - ); - }); + ).to.be.fulfilled); - it('should correctly validate a contract with extra fields', () => { - assert( + it('should correctly validate a contract with extra fields', () => + expect( validateContract({ slug: 'user-container', name: 'user-container', version: '3.0.0', }), - ); - }); + ).to.be.fulfilled); 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(); + return Promise.all([ + expect(validateContract({})).to.be.rejected, + expect(validateContract({ name: 'test' })).to.be.rejected, + expect(validateContract({ requires: [] })).to.be.rejected, + ]); }); - it('should correctly validate a contract with requirements', () => { - assert( + it('should correctly validate a contract with requirements', () => + expect( validateContract({ slug: 'user-container', requires: [ { - type: 'sw.os', - version: '>3.0.0', + type: 'sw.l4t', + version: '32.2', }, { type: 'sw.supervisor', }, ], }), - ); - }); + ).to.be.fulfilled); it('should not validate a contract with requirements without the minimum required fields', () => { - expect(() => + return expect( validateContract({ slug: 'user-container', requires: [ @@ -69,7 +78,7 @@ describe('Container contracts', () => { }, ], }), - ).to.throw(); + ).to.be.rejected; }); }); @@ -140,8 +149,8 @@ describe('Container contracts', () => { slug: 'user-container', requires: [ { - type: 'sw.os', - version: '>2.0.0', + type: 'sw.supervisor', + version: `>${supervisorVersionLesser}`, }, ], }, @@ -207,8 +216,8 @@ describe('Container contracts', () => { version: `>${supervisorVersionLesser}`, }, { - type: 'sw.os', - version: '<3.0.0', + type: 'sw.l4t', + version: '32.2', }, ], }, @@ -263,8 +272,8 @@ describe('Container contracts', () => { slug: 'user-container', requires: [ { - type: 'sw.os', - version: '>=3.0.0', + type: 'sw.supervisor', + version: `>=${supervisorVersionGreater}`, }, ], }, @@ -286,12 +295,8 @@ describe('Container contracts', () => { slug: 'user-container2', requires: [ { - type: 'sw.supervisor', - version: `>=${supervisorVersionLesser}`, - }, - { - type: 'sw.os', - version: '>3.0.0', + type: 'sw.l4t', + version: '28.2', }, ], }, @@ -356,7 +361,7 @@ describe('Container contracts', () => { slug: 'service1', requires: [ { - type: 'sw.os', + type: 'sw.supervisor', version: `<${supervisorVersionGreater}`, }, ], @@ -381,7 +386,7 @@ describe('Container contracts', () => { slug: 'service1', requires: [ { - type: 'sw.os', + type: 'sw.supervisor', version: `>${supervisorVersionGreater}`, }, ], @@ -403,3 +408,38 @@ describe('Container contracts', () => { }); }); }); + +describe('L4T version detection', () => { + it('should correctly parse L4T version strings', async () => { + let execStub = stub(child_process, 'exec').returns( + Promise.resolve([ + Buffer.from('4.9.140-l4t-r32.2+g3dcbed5'), + Buffer.from(''), + ]), + ); + + expect(await osRelease.getL4tVersion()).to.equal('32.2'); + expect(execStub.callCount).to.equal(1); + + execStub.restore(); + + execStub = stub(child_process, 'exec').returns( + Promise.resolve([ + Buffer.from('4.4.38-l4t-r28.2+g174510d'), + Buffer.from(''), + ]), + ); + expect(await osRelease.getL4tVersion()).to.equal('28.2'); + expect(execStub.callCount).to.equal(1); + execStub.restore(); + }); + + it('should return undefined when there is no l4t string in uname', async () => { + const execStub = stub(child_process, 'exec').returns( + Promise.resolve([Buffer.from('4.18.14-yocto-standard'), Buffer.from('')]), + ); + + expect(await osRelease.getL4tVersion()).to.be.undefined; + execStub.restore(); + }); +});