@toa.io/norm 1.0.0-alpha.3 → 1.0.0-alpha.300
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/package.json +9 -7
- package/src/.component/.expand/bridge.js +2 -6
- package/src/.component/.expand/entity.js +2 -9
- package/src/.component/.expand/events.js +2 -6
- package/src/.component/.expand/extensions.js +5 -8
- package/src/.component/.expand/index.js +7 -17
- package/src/.component/.expand/operations.js +2 -9
- package/src/.component/.expand/receivers.js +4 -7
- package/src/.component/.expand/version.js +55 -0
- package/src/.component/.normalize/entity.js +7 -0
- package/src/.component/.normalize/events.js +16 -4
- package/src/.component/.normalize/index.js +4 -9
- package/src/.component/.normalize/operations.js +10 -6
- package/src/.component/.normalize/receivers.js +4 -5
- package/src/.component/collapse.js +60 -9
- package/src/.component/defaults.js +16 -25
- package/src/.component/dependencies.js +14 -10
- package/src/.component/dereference.js +20 -18
- package/src/.component/expand.js +6 -9
- package/src/.component/extensions.js +9 -10
- package/src/.component/index.js +10 -21
- package/src/.component/merge.js +25 -13
- package/src/.component/migrations.js +57 -0
- package/src/.component/normalize.js +10 -6
- package/src/.component/schema.yaml +139 -59
- package/src/.component/validate.js +37 -17
- package/src/.context/.dependencies/connectors.js +11 -10
- package/src/.context/.dependencies/describe.js +4 -5
- package/src/.context/.dependencies/extensions.js +93 -20
- package/src/.context/.dependencies/index.js +3 -9
- package/src/.context/.dependencies/resolve.js +44 -7
- package/src/.context/complete.js +1 -5
- package/src/.context/converge.js +31 -0
- package/src/.context/dependencies.js +43 -6
- package/src/.context/dereference.js +11 -6
- package/src/.context/evict.js +38 -0
- package/src/.context/expand.js +11 -6
- package/src/.context/index.js +8 -15
- package/src/.context/normalize.js +9 -7
- package/src/.context/schema.yaml +179 -11
- package/src/.context/validate.js +8 -14
- package/src/component.js +37 -18
- package/src/context.js +33 -14
- package/src/definition.js +56 -0
- package/src/entity.js +15 -0
- package/src/index.js +6 -9
- package/src/manifest.js +154 -0
- package/src/shortcuts.js +9 -14
- package/test/component/collapse.fixtures.js +30 -39
- package/test/component/collapse.test.js +90 -10
- package/test/component/dependencies.test.js +16 -14
- package/test/component/dereference.fixtures.js +15 -24
- package/test/component/dereference.test.js +18 -8
- package/test/component/dummies/extension/index.js +1 -5
- package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
- package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
- package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
- package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
- package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
- package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
- package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
- package/test/component/expand.fixtures.js +13 -12
- package/test/component/expand.test.js +17 -13
- package/test/component/migrations.test.js +62 -0
- package/test/component/normalize.fixtures.js +3 -7
- package/test/component/normalize.test.js +86 -28
- package/test/component/validate.fixtures.js +15 -13
- package/test/component/validate.test.js +181 -107
- package/test/context/complete.fixtures.js +2 -8
- package/test/context/complete.test.js +14 -8
- package/test/context/converge.test.js +57 -0
- package/test/context/dereference.fixtures.js +3 -9
- package/test/context/dereference.test.js +18 -7
- package/test/context/evict.fixtures.js +39 -0
- package/test/context/evict.test.js +72 -0
- package/test/context/expand.fixtures.js +1 -5
- package/test/context/expand.test.js +36 -8
- package/test/context/normalize.fixtures.js +2 -7
- package/test/context/normalize.test.js +33 -9
- package/test/context/validate.fixtures.js +1 -6
- package/test/context/validate.test.js +143 -34
- package/test/definition.test.js +35 -0
- package/test/dummies/bare/index.js +1 -0
- package/test/dummies/defined/definition.js +1 -0
- package/test/dummies/defined/index.js +1 -0
- package/test/dummies/defined/package.json +1 -0
- package/test/manifest.test.js +126 -0
- package/test/shortcuts.fixtures.js +4 -9
- package/test/shortcuts.test.js +17 -15
- package/types/component.d.ts +24 -5
- package/types/context/declaration.d.ts +33 -3
- package/types/context.d.ts +19 -13
- package/types/entity.d.ts +7 -0
- package/types/index.d.ts +12 -3
- package/src/.component/.expand/properties.js +0 -14
- package/src/.context/.dependencies/load.js +0 -36
- package/test/context/dependencies.load.test.js +0 -24
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { definition } from '../../definition.js'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const resolve = (references, annotations) => {
|
|
3
|
+
export const resolve = async (references, annotations = {}, context = {}) => {
|
|
6
4
|
const dependencies = {}
|
|
7
5
|
|
|
8
6
|
for (const [dependency, components] of Object.entries(references)) {
|
|
9
|
-
const {
|
|
10
|
-
const id = metadata.name
|
|
7
|
+
const { name: id, module } = await definition(dependency)
|
|
11
8
|
|
|
12
9
|
const instances = components.map((component) => ({
|
|
13
10
|
locator: component.locator,
|
|
@@ -22,9 +19,49 @@ const resolve = (references, annotations) => {
|
|
|
22
19
|
if (annotation !== undefined && module.annotation !== undefined) {
|
|
23
20
|
annotations[id] = module.annotation(annotation, instances)
|
|
24
21
|
}
|
|
22
|
+
|
|
23
|
+
// what a declaration costs to install, read from the definition rather than from the
|
|
24
|
+
// package: a deploy installs it, and nothing that declares nothing carries it. A component
|
|
25
|
+
// is asked for its own instance; a composition that runs the package's service is asked
|
|
26
|
+
// with none, because what a service brings is not a component's to declare
|
|
27
|
+
if (module.installs !== undefined) {
|
|
28
|
+
for (const instance of instances)
|
|
29
|
+
Object.assign(
|
|
30
|
+
(instance.component.packages ??= {}),
|
|
31
|
+
module.installs(instance, annotations[id])
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const service = module.installs(undefined, annotations[id])
|
|
35
|
+
|
|
36
|
+
// mono runs every service in one process, so it takes every service's packages
|
|
37
|
+
Object.assign((context.packages ??= {}), service)
|
|
38
|
+
|
|
39
|
+
for (const composition of context.compositions ?? [])
|
|
40
|
+
if (composition.services?.includes(dependency) === true)
|
|
41
|
+
Object.assign((composition.packages ??= {}), service)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const dependency of Object.keys(annotations)) {
|
|
46
|
+
if (dependency in dependencies) continue
|
|
47
|
+
|
|
48
|
+
// an annotation may be keyed by a dependency id rather than by a module reference
|
|
49
|
+
const module = await optional(dependency)
|
|
50
|
+
|
|
51
|
+
if (module?.standalone === true) dependencies[dependency] = []
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
return dependencies
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} reference
|
|
59
|
+
* @returns {object | null}
|
|
60
|
+
*/
|
|
61
|
+
async function optional(reference) {
|
|
62
|
+
try {
|
|
63
|
+
return (await definition(reference)).module
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/.context/complete.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Completes missing compositions with unused components
|
|
5
3
|
* @param {toa.norm.Context} context
|
|
6
4
|
* @returns {void}
|
|
7
5
|
*/
|
|
8
|
-
const complete = (context) => {
|
|
6
|
+
export const complete = (context) => {
|
|
9
7
|
/** @type {Set<string>} */
|
|
10
8
|
const composed = new Set()
|
|
11
9
|
|
|
@@ -29,5 +27,3 @@ const complete = (context) => {
|
|
|
29
27
|
context.compositions.push({ name, components: [component] })
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
|
-
|
|
33
|
-
exports.complete = complete
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A context that declares convergence converges every component that stores anything, and no
|
|
3
|
+
* manifest says so. An extension is loaded, deployed and hooked because a component's
|
|
4
|
+
* `extensions` names it, so this is where they come to name it.
|
|
5
|
+
*
|
|
6
|
+
* **Including the components an extension ships**, which is why this runs where they are
|
|
7
|
+
* known rather than over what the context declares: an application deployed as two regions
|
|
8
|
+
* whose identity does not converge is one where a user registered in the first does not
|
|
9
|
+
* exist in the second.
|
|
10
|
+
*
|
|
11
|
+
* There is nothing per-component to normalize and nothing to opt out of: a region is
|
|
12
|
+
* infrastructure, and what a component stores is not its business. What is genuinely owned by
|
|
13
|
+
* one region — which delayed calls this one makes — is said where that is decided, in
|
|
14
|
+
* `cadence.regions`, and not by holding the records back.
|
|
15
|
+
*
|
|
16
|
+
* @param {toa.norm.Context} context
|
|
17
|
+
* @param {toa.norm.Component[]} components every component the context deploys
|
|
18
|
+
* @returns {void}
|
|
19
|
+
*/
|
|
20
|
+
export const converge = (context, components) => {
|
|
21
|
+
if (context.annotations?.[REFERENCE] === undefined) return
|
|
22
|
+
|
|
23
|
+
for (const component of components) {
|
|
24
|
+
if (component.entity === undefined) continue
|
|
25
|
+
|
|
26
|
+
component.extensions ??= {}
|
|
27
|
+
component.extensions[REFERENCE] ??= null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const REFERENCE = '@toa.io/extensions.convergence'
|
|
@@ -1,13 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
import { connectors, extensions, resolve } from './.dependencies/index.js'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
export const dependencies = async (context) => {
|
|
4
|
+
const { extensions: e, components, contributed } = await extensions(context)
|
|
5
|
+
|
|
6
|
+
unmanaged(context, contributed)
|
|
4
7
|
|
|
5
|
-
const dependencies = async (context) => {
|
|
6
|
-
const { extensions: e, components } = await extensions(context)
|
|
7
8
|
const c = connectors(context, components)
|
|
8
9
|
const references = { ...c, ...e }
|
|
9
10
|
|
|
10
|
-
return resolve(references, context.annotations)
|
|
11
|
+
return resolve(references, context.annotations, context)
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Marks as evicted what only evicted components require: the components an extension brings,
|
|
16
|
+
* where nothing a deployment carries references the extension and no composition runs it. A
|
|
17
|
+
* storage or a binding needs no mark of its own — its instances are components, and a
|
|
18
|
+
* deployment reads theirs.
|
|
19
|
+
*
|
|
20
|
+
* @param {toa.norm.Context} context
|
|
21
|
+
* @param {Map<string, toa.norm.Component[]>} contributed
|
|
22
|
+
* @returns {void}
|
|
23
|
+
*/
|
|
24
|
+
function unmanaged(context, contributed) {
|
|
25
|
+
const components = context.components ?? []
|
|
26
|
+
|
|
27
|
+
if (!components.some((component) => component.evicted === true)) return
|
|
28
|
+
|
|
29
|
+
const required = new Set()
|
|
30
|
+
const pending = components.filter((component) => component.evicted !== true)
|
|
31
|
+
|
|
32
|
+
// what a composition runs is deployed for being listed, whoever references it
|
|
33
|
+
for (const composition of context.compositions ?? [])
|
|
34
|
+
for (const reference of composition.services ?? [])
|
|
35
|
+
pending.push(...(contributed.get(reference) ?? []))
|
|
36
|
+
|
|
37
|
+
while (pending.length > 0) {
|
|
38
|
+
const component = pending.pop()
|
|
39
|
+
|
|
40
|
+
if (required.has(component)) continue
|
|
41
|
+
|
|
42
|
+
required.add(component)
|
|
43
|
+
|
|
44
|
+
for (const reference of Object.keys(component.extensions ?? {}))
|
|
45
|
+
pending.push(...(contributed.get(reference) ?? []))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const brought of contributed.values())
|
|
49
|
+
for (const component of brought) if (!required.has(component)) component.evicted = true
|
|
50
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Resolves component IDs within compositions with Component objects
|
|
5
3
|
* @param {toa.norm.Context} context
|
|
6
4
|
* @returns {void}
|
|
7
5
|
*/
|
|
8
|
-
const dereference = (context) => {
|
|
6
|
+
export const dereference = (context) => {
|
|
9
7
|
const components = map(context.components)
|
|
10
8
|
|
|
11
9
|
if (context.compositions !== undefined) {
|
|
12
10
|
for (const composition of context.compositions) {
|
|
13
|
-
composition.components = composition.components.map((id) =>
|
|
11
|
+
composition.components = composition.components.map((id) => {
|
|
12
|
+
const component = components[id]
|
|
13
|
+
|
|
14
|
+
if (component === undefined)
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Composition '${composition.name}' lists an unknown component '${id}'.`
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return component
|
|
20
|
+
})
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
23
|
}
|
|
@@ -27,5 +34,3 @@ const map = (components) => {
|
|
|
27
34
|
|
|
28
35
|
return map
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
exports.dereference = dereference
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks what the context evicts. An evicted component stays in the context like any other — it
|
|
3
|
+
* is called, its types are written, what it receives is published — and only a deployment
|
|
4
|
+
* leaves it out, because something else deploys it. What only evicted components require is
|
|
5
|
+
* marked once the extensions are extracted, by `dependencies`.
|
|
6
|
+
*
|
|
7
|
+
* An evicted service is taken off the compositions that list it, so that none of them runs it.
|
|
8
|
+
*
|
|
9
|
+
* @param {toa.norm.Context} context
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
12
|
+
export const evict = (context) => {
|
|
13
|
+
if (context.evicted === undefined) return
|
|
14
|
+
|
|
15
|
+
const components = new Set(context.evicted.components ?? [])
|
|
16
|
+
const services = new Set(context.evicted.services ?? [])
|
|
17
|
+
|
|
18
|
+
if (components.size > 0) {
|
|
19
|
+
const present = new Set(context.components.map((component) => component.locator.id))
|
|
20
|
+
|
|
21
|
+
for (const id of components)
|
|
22
|
+
if (!present.has(id))
|
|
23
|
+
throw new Error(`'evicted' names an unknown component '${id}'.`)
|
|
24
|
+
|
|
25
|
+
for (const component of context.components)
|
|
26
|
+
if (components.has(component.locator.id)) component.evicted = true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (context.compositions === undefined) return
|
|
30
|
+
|
|
31
|
+
for (const composition of context.compositions) {
|
|
32
|
+
if (composition.services === undefined) continue
|
|
33
|
+
|
|
34
|
+
composition.services = composition.services.filter((reference) => !services.has(reference))
|
|
35
|
+
|
|
36
|
+
if (composition.services.length === 0) delete composition.services
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/.context/expand.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const shortcuts = require('../shortcuts')
|
|
1
|
+
import * as shortcuts from '../shortcuts.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* @param {toa.norm.context.Declaration | object} context
|
|
7
5
|
*/
|
|
8
|
-
const expand = (context) => {
|
|
6
|
+
export const expand = (context) => {
|
|
9
7
|
shortcuts.recognize(shortcuts.SHORTCUTS, context, 'annotations')
|
|
10
8
|
shortcuts.recognize(shortcuts.SHORTCUTS, context.annotations)
|
|
11
|
-
}
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
// a composition names a service the way a manifest names an extension, and everything
|
|
11
|
+
// downstream matches it against a dependency, which is keyed by package reference
|
|
12
|
+
for (const composition of context.compositions ?? [])
|
|
13
|
+
if (composition.services !== undefined)
|
|
14
|
+
composition.services = composition.services.map(shortcuts.resolve)
|
|
15
|
+
|
|
16
|
+
if (context.evicted?.services !== undefined)
|
|
17
|
+
context.evicted.services = context.evicted.services.map(shortcuts.resolve)
|
|
18
|
+
}
|
package/src/.context/index.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
exports.complete = complete
|
|
11
|
-
exports.dependencies = dependencies
|
|
12
|
-
exports.dereference = dereference
|
|
13
|
-
exports.expand = expand
|
|
14
|
-
exports.normalize = normalize
|
|
15
|
-
exports.validate = validate
|
|
1
|
+
export { complete } from './complete.js'
|
|
2
|
+
export { converge } from './converge.js'
|
|
3
|
+
export { dependencies } from './dependencies.js'
|
|
4
|
+
export { dereference } from './dereference.js'
|
|
5
|
+
export { evict } from './evict.js'
|
|
6
|
+
export { expand } from './expand.js'
|
|
7
|
+
export { normalize } from './normalize.js'
|
|
8
|
+
export { validate } from './validate.js'
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import * as definitions from '@toa.io/definitions'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @param {toa.norm.context.Declaration | Object} context
|
|
5
5
|
*/
|
|
6
|
-
const normalize = (context) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (context.runtime === undefined) context.runtime = { version: runtime.version }
|
|
6
|
+
export const normalize = (context) => {
|
|
7
|
+
if (context.runtime === undefined) context.runtime = { version: definitions.version }
|
|
10
8
|
if (typeof context.runtime === 'string') context.runtime = { version: context.runtime }
|
|
11
9
|
|
|
12
10
|
if (context.runtime.version === undefined || context.runtime.version === '.') {
|
|
13
|
-
context.runtime.version =
|
|
11
|
+
context.runtime.version = definitions.version
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
if (typeof context.registry === 'string') context.registry = { base: context.registry }
|
|
15
|
+
|
|
16
|
+
// what an image is built for, where the Context does not say; `~` says the runner's own
|
|
17
|
+
if (context.registry !== undefined && context.registry.platforms === undefined)
|
|
18
|
+
context.registry.platforms = PLATFORMS
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
const PLATFORMS = ['linux/amd64', 'linux/arm/v7', 'linux/arm64']
|
package/src/.context/schema.yaml
CHANGED
|
@@ -6,10 +6,9 @@ properties:
|
|
|
6
6
|
version:
|
|
7
7
|
type: string
|
|
8
8
|
name:
|
|
9
|
-
$ref: 'definitions#/definitions/label'
|
|
10
|
-
description:
|
|
11
9
|
type: string
|
|
12
|
-
|
|
10
|
+
pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(-([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))*$
|
|
11
|
+
description:
|
|
13
12
|
type: string
|
|
14
13
|
build:
|
|
15
14
|
type: object
|
|
@@ -41,28 +40,197 @@ properties:
|
|
|
41
40
|
minItems: 1
|
|
42
41
|
items:
|
|
43
42
|
type: string
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
services:
|
|
44
|
+
description: >
|
|
45
|
+
Where an extension service's image comes from. `build`, which absence means,
|
|
46
|
+
builds it into `registry.base` the way a composition is built. `published` uses
|
|
47
|
+
the image the extension ships, which is neither built nor pushed.
|
|
48
|
+
enum: [build, published]
|
|
49
|
+
# required: [base]
|
|
49
50
|
compositions:
|
|
50
51
|
type: array
|
|
51
52
|
minItems: 1
|
|
52
53
|
items:
|
|
53
54
|
type: object
|
|
55
|
+
additionalProperties: false
|
|
54
56
|
properties:
|
|
55
57
|
name:
|
|
56
|
-
|
|
58
|
+
type: string
|
|
59
|
+
pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
|
|
57
60
|
image:
|
|
58
61
|
type: string
|
|
59
62
|
components:
|
|
60
63
|
type: array
|
|
61
64
|
minItems: 1
|
|
62
65
|
items:
|
|
63
|
-
|
|
66
|
+
type: string
|
|
67
|
+
pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
|
|
68
|
+
services:
|
|
69
|
+
description: >
|
|
70
|
+
Extension services this composition runs in its own pod, named by shortcut or
|
|
71
|
+
package reference, rather than each as its own deployment. A service listed here
|
|
72
|
+
keeps its own Service and Ingress; what changes is the pod they select.
|
|
73
|
+
type: array
|
|
74
|
+
minItems: 1
|
|
75
|
+
items:
|
|
76
|
+
type: string
|
|
77
|
+
minLength: 1
|
|
78
|
+
resources:
|
|
79
|
+
$ref: '#/definitions/resources'
|
|
64
80
|
required: [name, components]
|
|
81
|
+
evicted:
|
|
82
|
+
description: >
|
|
83
|
+
What Toa does not deploy for this context, even where a composition lists it: it stays
|
|
84
|
+
part of the context, and is deployed by other means. A component named
|
|
85
|
+
here is in no pod, and what only it declared — an extension, a storage, a binding — is
|
|
86
|
+
not deployed either. A service named here gets no Deployment, Service or Ingress, and
|
|
87
|
+
no composition runs it.
|
|
88
|
+
type: object
|
|
89
|
+
additionalProperties: false
|
|
90
|
+
minProperties: 1
|
|
91
|
+
properties:
|
|
92
|
+
components:
|
|
93
|
+
type: array
|
|
94
|
+
minItems: 1
|
|
95
|
+
items:
|
|
96
|
+
type: string
|
|
97
|
+
pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
|
|
98
|
+
services:
|
|
99
|
+
type: array
|
|
100
|
+
minItems: 1
|
|
101
|
+
items:
|
|
102
|
+
type: string
|
|
103
|
+
minLength: 1
|
|
104
|
+
atomicity:
|
|
105
|
+
description: >
|
|
106
|
+
Where the replicas of a component decide together: who owns what, what they have spent
|
|
107
|
+
between them, and which of them holds a name while it works. Absent or unreachable,
|
|
108
|
+
nothing is owned and whoever asked stands down; metering and locking are refused.
|
|
109
|
+
type: object
|
|
110
|
+
additionalProperties: false
|
|
111
|
+
properties:
|
|
112
|
+
redis:
|
|
113
|
+
description: >
|
|
114
|
+
One address, or an odd number of them — independent servers, not the nodes of a
|
|
115
|
+
cluster. A lock is taken on a quorum of them; the registry and the meter use the
|
|
116
|
+
first, because each of them counts on a single key.
|
|
117
|
+
oneOf:
|
|
118
|
+
- type: string
|
|
119
|
+
- type: array
|
|
120
|
+
minItems: 1
|
|
121
|
+
items:
|
|
122
|
+
type: string
|
|
123
|
+
interval:
|
|
124
|
+
description: >
|
|
125
|
+
How often a replica registers, in milliseconds. It has to comfortably exceed a round
|
|
126
|
+
trip to Redis and the jitter around it, and nothing is owned until two consecutive
|
|
127
|
+
intervals have agreed — so this is also how long a replica owns nothing after it
|
|
128
|
+
joins, restarts or stalls.
|
|
129
|
+
type: integer
|
|
130
|
+
minimum: 100
|
|
131
|
+
required: [redis]
|
|
132
|
+
outbox:
|
|
133
|
+
type: object
|
|
134
|
+
additionalProperties: false
|
|
135
|
+
properties:
|
|
136
|
+
interval:
|
|
137
|
+
description: >
|
|
138
|
+
The pump's cycle, in milliseconds. One cycle reads what is unpublished, publishes it
|
|
139
|
+
and marks it. In steady state it reads nothing, so this is budgeted for how fast a rare
|
|
140
|
+
failure heals rather than for how fast events flow.
|
|
141
|
+
type: integer
|
|
142
|
+
minimum: 100
|
|
143
|
+
batch:
|
|
144
|
+
description: >
|
|
145
|
+
How many rows one read brings back. A page that comes back full is read on from,
|
|
146
|
+
so this bounds a read rather than a cycle.
|
|
147
|
+
type: integer
|
|
148
|
+
minimum: 1
|
|
149
|
+
retention:
|
|
150
|
+
description: >
|
|
151
|
+
Seconds a published row is kept as a change log before it expires. A row that never
|
|
152
|
+
made it out has no expiry and is never reaped.
|
|
153
|
+
type: integer
|
|
154
|
+
minimum: 0
|
|
155
|
+
inbox:
|
|
156
|
+
type: object
|
|
157
|
+
additionalProperties: false
|
|
158
|
+
properties:
|
|
159
|
+
retention:
|
|
160
|
+
description: >
|
|
161
|
+
Seconds a call is remembered, which is the window a duplicate of it is caught in. Budget
|
|
162
|
+
it for how late a duplicate can arrive — a redelivery, the attempts the broker library
|
|
163
|
+
makes of a message, whatever a client retries on — rather than for how long the change
|
|
164
|
+
matters. Every call of an operation that declares `once` is a record for this long.
|
|
165
|
+
type: integer
|
|
166
|
+
minimum: 1
|
|
167
|
+
addressed:
|
|
168
|
+
type: object
|
|
169
|
+
additionalProperties: false
|
|
170
|
+
properties:
|
|
171
|
+
timeout:
|
|
172
|
+
description: >
|
|
173
|
+
Milliseconds an addressed call waits for its reply where it names no timeout of its own.
|
|
174
|
+
Nothing else ends a call to a process that has gone, so every addressed call waits for a
|
|
175
|
+
set time.
|
|
176
|
+
type: integer
|
|
177
|
+
minimum: 1
|
|
178
|
+
events:
|
|
179
|
+
description: >
|
|
180
|
+
Events of this context that are consumed outside it. An event is otherwise published only
|
|
181
|
+
where something in the context receives it.
|
|
182
|
+
type: array
|
|
183
|
+
items:
|
|
184
|
+
type: string
|
|
185
|
+
pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$
|
|
186
|
+
mono:
|
|
187
|
+
type: object
|
|
188
|
+
additionalProperties: false
|
|
189
|
+
properties:
|
|
190
|
+
replicas:
|
|
191
|
+
type: integer
|
|
192
|
+
minimum: 1
|
|
193
|
+
resources:
|
|
194
|
+
$ref: '#/definitions/resources'
|
|
195
|
+
ingress:
|
|
196
|
+
type: object
|
|
197
|
+
additionalProperties: false
|
|
198
|
+
properties:
|
|
199
|
+
hosts:
|
|
200
|
+
type: array
|
|
201
|
+
minItems: 1
|
|
202
|
+
items:
|
|
203
|
+
type: string
|
|
204
|
+
class:
|
|
205
|
+
type: string
|
|
206
|
+
annotations:
|
|
207
|
+
type: object
|
|
208
|
+
default:
|
|
209
|
+
type: boolean
|
|
65
210
|
annotations:
|
|
66
211
|
type: object
|
|
67
|
-
|
|
212
|
+
resources:
|
|
213
|
+
$ref: '#/definitions/resources'
|
|
214
|
+
required: [runtime, name, registry]
|
|
68
215
|
additionalProperties: false
|
|
216
|
+
|
|
217
|
+
definitions:
|
|
218
|
+
# What a deployment may take. `null` deploys it without any, which is a decision
|
|
219
|
+
# rather than an omission — every deployment has to make one.
|
|
220
|
+
resources:
|
|
221
|
+
type: object
|
|
222
|
+
nullable: true
|
|
223
|
+
additionalProperties: false
|
|
224
|
+
properties:
|
|
225
|
+
cpu:
|
|
226
|
+
type: array
|
|
227
|
+
items:
|
|
228
|
+
type: string
|
|
229
|
+
minItems: 2
|
|
230
|
+
maxItems: 2
|
|
231
|
+
memory:
|
|
232
|
+
type: array
|
|
233
|
+
items:
|
|
234
|
+
type: string
|
|
235
|
+
minItems: 2
|
|
236
|
+
maxItems: 2
|
package/src/.context/validate.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { yaml } from '@toa.io/generic'
|
|
5
|
+
import * as schemas from '@toa.io/schemas'
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
+
const path = resolve(import.meta.dirname, 'schema.yaml')
|
|
8
|
+
const object = yaml.load(readFileSync(path, 'utf8'))
|
|
9
|
+
const schema = schemas.schema(object)
|
|
7
10
|
|
|
8
|
-
const
|
|
9
|
-
const object = load.sync(path)
|
|
10
|
-
const schema = new Schema(object)
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param {toa.norm.context.Declaration} context
|
|
14
|
-
*/
|
|
15
|
-
const validate = (context) => {
|
|
11
|
+
export const validate = (context) => {
|
|
16
12
|
schema.validate(context)
|
|
17
13
|
}
|
|
18
|
-
|
|
19
|
-
exports.validate = validate
|
package/src/component.js
CHANGED
|
@@ -1,49 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import { join } from 'node:path'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { yaml as jsyaml } from '@toa.io/generic'
|
|
5
|
+
import { find } from '@toa.io/generic'
|
|
6
|
+
import { Locator } from '@toa.io/core'
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
const { directory: { find } } = require('@toa.io/filesystem')
|
|
7
|
-
const { Locator } = require('@toa.io/core')
|
|
8
|
-
|
|
9
|
-
const {
|
|
8
|
+
import {
|
|
10
9
|
expand,
|
|
11
10
|
merge,
|
|
11
|
+
migrations,
|
|
12
12
|
validate,
|
|
13
13
|
collapse,
|
|
14
14
|
dereference,
|
|
15
15
|
defaults,
|
|
16
16
|
normalize,
|
|
17
17
|
extensions
|
|
18
|
-
}
|
|
18
|
+
} from './.component/index.js'
|
|
19
19
|
|
|
20
|
-
const component = async (path) => {
|
|
20
|
+
export const component = async (path) => {
|
|
21
21
|
const manifest = await load(path)
|
|
22
22
|
|
|
23
|
-
normalize(manifest)
|
|
24
|
-
validate(manifest)
|
|
25
|
-
extensions(manifest)
|
|
23
|
+
await normalize(manifest, path)
|
|
24
|
+
await validate(manifest)
|
|
25
|
+
await extensions(manifest)
|
|
26
26
|
|
|
27
27
|
manifest.locator = new Locator(manifest.name, manifest.namespace)
|
|
28
28
|
|
|
29
29
|
return manifest
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const load = async (path, base) => {
|
|
32
|
+
const load = async (path, base, proto = false) => {
|
|
33
33
|
if (base !== undefined) path = find(path, base, MANIFEST)
|
|
34
34
|
|
|
35
35
|
const file = join(path, MANIFEST)
|
|
36
|
-
const manifest =
|
|
36
|
+
const manifest = (await read(file)) ?? {}
|
|
37
37
|
|
|
38
38
|
manifest.path = path
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const anonymous = manifest.name === undefined
|
|
41
|
+
|
|
42
|
+
defaults(manifest, proto)
|
|
43
|
+
await expand(manifest)
|
|
42
44
|
|
|
43
45
|
await merge(path, manifest)
|
|
46
|
+
await migrations(path, manifest)
|
|
47
|
+
|
|
48
|
+
// an inherited migration is recorded under the prototype's name, and a generated one is not
|
|
49
|
+
// the same on the next machine
|
|
50
|
+
if (proto && anonymous && manifest.entity?.migrations !== undefined)
|
|
51
|
+
throw new Error(`Prototype at '${path}' declares migrations, so it has to be named`)
|
|
44
52
|
|
|
45
53
|
if (manifest.prototype !== null) {
|
|
46
|
-
const prototype = await load(manifest.prototype, path)
|
|
54
|
+
const prototype = await load(manifest.prototype, path, true)
|
|
47
55
|
|
|
48
56
|
collapse(manifest, prototype)
|
|
49
57
|
}
|
|
@@ -56,4 +64,15 @@ const load = async (path, base) => {
|
|
|
56
64
|
|
|
57
65
|
const MANIFEST = 'manifest.toa.yaml'
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Reads a YAML file, resolving anchors into distinct objects so that
|
|
69
|
+
* mutating one node cannot reach another.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} path
|
|
72
|
+
* @return {Promise<object>}
|
|
73
|
+
*/
|
|
74
|
+
async function read(path) {
|
|
75
|
+
const object = jsyaml.load(await readFile(path, 'utf8'))
|
|
76
|
+
|
|
77
|
+
return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
|
|
78
|
+
}
|