@toa.io/norm 1.0.0-alpha.29 → 1.0.0-alpha.291

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +8 -7
  3. package/src/.component/.expand/bridge.js +2 -6
  4. package/src/.component/.expand/entity.js +2 -9
  5. package/src/.component/.expand/events.js +2 -6
  6. package/src/.component/.expand/extensions.js +5 -8
  7. package/src/.component/.expand/index.js +8 -19
  8. package/src/.component/.expand/operations.js +2 -9
  9. package/src/.component/.expand/properties.js +2 -6
  10. package/src/.component/.expand/receivers.js +4 -7
  11. package/src/.component/.expand/version.js +38 -28
  12. package/src/.component/.normalize/entity.js +7 -0
  13. package/src/.component/.normalize/events.js +15 -5
  14. package/src/.component/.normalize/index.js +4 -9
  15. package/src/.component/.normalize/operations.js +10 -6
  16. package/src/.component/.normalize/receivers.js +4 -5
  17. package/src/.component/collapse.js +60 -9
  18. package/src/.component/defaults.js +16 -10
  19. package/src/.component/dependencies.js +14 -10
  20. package/src/.component/dereference.js +20 -18
  21. package/src/.component/expand.js +3 -7
  22. package/src/.component/extensions.js +15 -8
  23. package/src/.component/index.js +10 -21
  24. package/src/.component/merge.js +24 -13
  25. package/src/.component/migrations.js +57 -0
  26. package/src/.component/normalize.js +10 -6
  27. package/src/.component/schema.yaml +108 -71
  28. package/src/.component/validate.js +36 -17
  29. package/src/.context/.dependencies/connectors.js +5 -9
  30. package/src/.context/.dependencies/describe.js +4 -5
  31. package/src/.context/.dependencies/extensions.js +62 -20
  32. package/src/.context/.dependencies/index.js +3 -9
  33. package/src/.context/.dependencies/load.js +17 -14
  34. package/src/.context/.dependencies/resolve.js +23 -6
  35. package/src/.context/complete.js +1 -5
  36. package/src/.context/dependencies.js +2 -6
  37. package/src/.context/dereference.js +11 -6
  38. package/src/.context/expand.js +8 -6
  39. package/src/.context/index.js +6 -15
  40. package/src/.context/normalize.js +7 -5
  41. package/src/.context/schema.yaml +132 -11
  42. package/src/.context/validate.js +8 -14
  43. package/src/component.js +36 -17
  44. package/src/context.js +26 -14
  45. package/src/entity.js +15 -0
  46. package/src/index.js +4 -9
  47. package/src/shortcuts.js +8 -13
  48. package/test/component/collapse.fixtures.js +30 -39
  49. package/test/component/collapse.test.js +90 -10
  50. package/test/component/dependencies.test.js +16 -14
  51. package/test/component/dereference.fixtures.js +15 -24
  52. package/test/component/dereference.test.js +18 -8
  53. package/test/component/dummies/extension/index.js +1 -5
  54. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
  55. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
  56. package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
  57. package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
  58. package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
  59. package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
  60. package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
  61. package/test/component/expand.fixtures.js +13 -13
  62. package/test/component/expand.test.js +25 -10
  63. package/test/component/migrations.test.js +62 -0
  64. package/test/component/normalize.fixtures.js +3 -7
  65. package/test/component/normalize.test.js +86 -28
  66. package/test/component/validate.fixtures.js +15 -13
  67. package/test/component/validate.test.js +162 -109
  68. package/test/context/complete.fixtures.js +2 -8
  69. package/test/context/complete.test.js +14 -8
  70. package/test/context/dependencies.load.test.js +13 -12
  71. package/test/context/dereference.fixtures.js +3 -9
  72. package/test/context/dereference.test.js +18 -7
  73. package/test/context/expand.fixtures.js +1 -5
  74. package/test/context/expand.test.js +36 -8
  75. package/test/context/normalize.fixtures.js +2 -7
  76. package/test/context/normalize.test.js +33 -9
  77. package/test/context/validate.fixtures.js +1 -6
  78. package/test/context/validate.test.js +81 -37
  79. package/test/shortcuts.fixtures.js +4 -9
  80. package/test/shortcuts.test.js +17 -15
  81. package/types/component.d.ts +17 -3
  82. package/types/context/declaration.d.ts +26 -3
  83. package/types/context.d.ts +15 -13
  84. package/types/entity.d.ts +7 -0
  85. package/types/index.d.ts +4 -3
@@ -1,25 +1,42 @@
1
- 'use strict'
1
+ import path from 'node:path'
2
2
 
3
- const path = require('node:path')
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 { load } = require('@toa.io/yaml')
6
- const { Schema } = require('@toa.io/schema')
7
+ const object = yaml.load(
8
+ readFileSync(path.resolve(import.meta.dirname, 'schema.yaml'), 'utf8')
9
+ )
10
+ const schema = schemas.schema(object)
7
11
 
8
- const object = load.sync(path.resolve(__dirname, 'schema.yaml'))
9
- const schema = new Schema(object)
10
-
11
- const validate = (manifest) => {
12
+ export const validate = async (manifest) => {
12
13
  const error = schema.fit(manifest)
13
14
 
14
- if (error) throw new Error(error.message)
15
+ if (error) throw error
15
16
 
16
- if (manifest.events !== undefined) events(manifest)
17
+ if (manifest.entity !== undefined) entity(manifest)
18
+ if (manifest.events !== undefined) await events(manifest)
17
19
  if (manifest.receivers !== undefined) receivers(manifest)
18
20
  }
19
21
 
20
- const events = (manifest) => {
22
+ /** What only the entity's own declaration can answer: whether a name it uses is one it declares. */
23
+ const entity = (manifest) => {
24
+ const { properties, required, blank } = manifest.entity
25
+
26
+ for (const name of required ?? [])
27
+ if (properties[name] === undefined)
28
+ throw new Error(`Entity requires property '${name}', which is not defined`)
29
+
30
+ for (const name of Object.keys(blank ?? {}))
31
+ if (properties[name] === undefined)
32
+ throw new Error(`Entity blank names property '${name}', which is not defined`)
33
+ }
34
+
35
+ const events = async (manifest) => {
21
36
  for (const [label, event] of Object.entries(manifest.events)) {
22
- if (require(event.binding).properties.async !== true) {
37
+ const { properties } = await import(event.binding)
38
+
39
+ if (properties.async !== true) {
23
40
  throw new Error(`Event '${label}' binding '${event.binding}' is not async`)
24
41
  }
25
42
  }
@@ -28,15 +45,17 @@ const events = (manifest) => {
28
45
  const receivers = (manifest) => {
29
46
  for (const [locator, receiver] of Object.entries(manifest.receivers)) {
30
47
  if (manifest.operations?.[receiver.operation] === undefined) {
31
- throw new Error(`Receiver '${locator}' refers to undefined operation '${receiver.operation}'`)
48
+ throw new Error(
49
+ `Receiver '${locator}' refers to undefined operation '${receiver.operation}'`
50
+ )
32
51
  }
33
52
 
34
53
  if (!TYPES.has(manifest.operations[receiver.operation].type)) {
35
- throw new Error(`Receiver '${locator}' must refer to an operation of one of the allowed types: ${Array.from(TYPES).join(', ')}`)
54
+ throw new Error(
55
+ `Receiver '${locator}' must refer to an operation of the allowed types: ${Array.from(TYPES).join(', ')}`
56
+ )
36
57
  }
37
58
  }
38
59
  }
39
60
 
40
- const TYPES = new Set(['transition', 'effect'])
41
-
42
- exports.validate = validate
61
+ const TYPES = new Set(['transition', 'assignment', 'effect', 'unmanaged'])
@@ -1,14 +1,12 @@
1
- 'use strict'
1
+ import { resolve } from 'node:path'
2
2
 
3
- const { resolve } = require('node:path')
4
-
5
- const connectors = (context, extracted) => {
3
+ export const connectors = (context, extracted) => {
6
4
  const connectors = {}
7
5
 
8
- const components = (context.components === undefined
6
+ const components =
7
+ (context.components === undefined
9
8
  ? extracted
10
- : context.components.concat(extracted)
11
- ) ?? []
9
+ : context.components.concat(extracted)) ?? []
12
10
 
13
11
  for (const component of components) {
14
12
  if (component.entity !== undefined) {
@@ -47,5 +45,3 @@ const connectors = (context, extracted) => {
47
45
 
48
46
  return connectors
49
47
  }
50
-
51
- exports.connectors = connectors
@@ -1,10 +1,9 @@
1
- 'use strict'
2
-
3
1
  /**
4
2
  * @param {toa.norm.Component} component
5
3
  * @param {Object} manifest
6
4
  * @returns {toa.norm.context.dependencies.Instance}
7
5
  */
8
- const describe = (component, manifest = undefined) => ({ locator: component.locator, manifest })
9
-
10
- exports.describe = describe
6
+ export const describe = (component, manifest = undefined) => ({
7
+ locator: component.locator,
8
+ manifest
9
+ })
@@ -1,9 +1,21 @@
1
- 'use strict'
1
+ import { component as load } from '../../component.js'
2
+ import { load as loadDependency } from './load.js'
2
3
 
3
- const extensions = async (context) => {
4
+ export const extensions = async (context) => {
4
5
  const extensions = {}
5
6
  const components = context.components?.slice() ?? []
6
- const extracted = await extractExtensionComponents(components, extensions)
7
+
8
+ // an extension a composition runs may be referenced by no component of this context,
9
+ // and its own components must be extracted all the same
10
+ const declared = await extractDeclaredServices(context, extensions)
11
+
12
+ const extracted = declared.concat(
13
+ await extractExtensionComponents(
14
+ components.concat(declared),
15
+ extensions,
16
+ context.annotations
17
+ )
18
+ )
7
19
 
8
20
  components.push(...extracted)
9
21
 
@@ -20,9 +32,28 @@ const extensions = async (context) => {
20
32
  return { extensions, components: extracted }
21
33
  }
22
34
 
23
- async function extractExtensionComponents (components, extensions) {
24
- const { component: load } = require('../../component')
35
+ async function extractDeclaredServices(context, extensions) {
36
+ const extracted = []
37
+
38
+ for (const composition of context.compositions ?? [])
39
+ for (const reference of composition.services ?? []) {
40
+ if (reference in extensions) continue
25
41
 
42
+ try {
43
+ extracted.push(...(await extract(reference, extensions, context.annotations)))
44
+ } catch (e) {
45
+ throw new Error(
46
+ `Composition '${composition.name}' lists '${reference}', ` +
47
+ `which cannot be resolved: ${e.message}`,
48
+ { cause: e }
49
+ )
50
+ }
51
+ }
52
+
53
+ return extracted
54
+ }
55
+
56
+ async function extractExtensionComponents(components, extensions, annotations) {
26
57
  const extracted = []
27
58
 
28
59
  for (const component of components) {
@@ -31,26 +62,37 @@ async function extractExtensionComponents (components, extensions) {
31
62
  for (const reference of Object.keys(component.extensions)) {
32
63
  if (reference in extensions) continue
33
64
 
34
- extensions[reference] = []
35
-
36
- const mod = require(reference)
37
-
38
- if (mod.components === undefined) continue
39
-
40
- for (const path of mod.components().paths) {
41
- const component = await load(path)
42
-
43
- extracted.push(component)
44
- }
65
+ extracted.push(...(await extract(reference, extensions, annotations)))
45
66
  }
46
67
  }
47
68
 
48
- if (extracted.length === 0)
49
- return extracted
69
+ if (extracted.length === 0) return extracted
50
70
 
51
- const deeper = await extractExtensionComponents(extracted, extensions)
71
+ const deeper = await extractExtensionComponents(extracted, extensions, annotations)
52
72
 
53
73
  return extracted.concat(deeper)
54
74
  }
55
75
 
56
- exports.extensions = extensions
76
+ /**
77
+ * The components an extension contributes, if it contributes any.
78
+ *
79
+ * @param {string} reference
80
+ * @param {object} extensions
81
+ * @param {object} [annotations]
82
+ * @returns {Promise<Array<toa.norm.Component>>}
83
+ */
84
+ async function extract(reference, extensions, annotations) {
85
+ extensions[reference] = []
86
+
87
+ const { metadata, module: mod } = await loadDependency(reference)
88
+
89
+ if (mod.components === undefined) return []
90
+
91
+ // the annotation decides whether an extension contributes components at all
92
+ const annotation = annotations?.[metadata?.name ?? reference]
93
+ const extracted = []
94
+
95
+ for (const path of mod.components(annotation).paths) extracted.push(await load(path))
96
+
97
+ return extracted
98
+ }
@@ -1,9 +1,3 @@
1
- 'use strict'
2
-
3
- const { connectors } = require('./connectors')
4
- const { extensions } = require('./extensions')
5
- const { resolve } = require('./resolve')
6
-
7
- exports.connectors = connectors
8
- exports.extensions = extensions
9
- exports.resolve = resolve
1
+ export { connectors } from './connectors.js'
2
+ export { extensions } from './extensions.js'
3
+ export { resolve } from './resolve.js'
@@ -1,23 +1,28 @@
1
- 'use strict'
1
+ import { createRequire } from 'node:module'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
2
5
 
3
- const { join } = require('node:path')
6
+ // import.meta.resolve takes no paths, and a dependency is named rather than
7
+ // written as a path
8
+ const require = createRequire(import.meta.url)
4
9
 
5
10
  /**
6
11
  * @param {string} path
7
- * @returns {{ metadata: object, module: object }}
12
+ * @returns {Promise<{ metadata: object, module: object }>}
8
13
  */
9
- function load (path) {
14
+ export async function load(path) {
10
15
  const metadata = loadMetadata(path)
11
- const module = loadModule(path)
16
+ const module = await loadModule(path)
12
17
 
13
18
  return { metadata, module }
14
19
  }
15
20
 
16
- function loadMetadata (reference) {
17
- reference = join(reference, 'package.json')
18
-
21
+ function loadMetadata(reference) {
19
22
  try {
20
- return require(reference)
23
+ return JSON.parse(
24
+ readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
25
+ )
21
26
  } catch {
22
27
  return null
23
28
  }
@@ -25,12 +30,10 @@ function loadMetadata (reference) {
25
30
 
26
31
  /**
27
32
  * @param {string} reference
28
- * @returns {object}
33
+ * @returns {Promise<object>}
29
34
  */
30
- function loadModule (reference) {
35
+ async function loadModule(reference) {
31
36
  const path = require.resolve(reference)
32
37
 
33
- return require(path)
38
+ return await import(pathToFileURL(path).href)
34
39
  }
35
-
36
- exports.load = load
@@ -1,12 +1,10 @@
1
- 'use strict'
1
+ import { load } from './load.js'
2
2
 
3
- const { load } = require('./load')
4
-
5
- const resolve = (references, annotations) => {
3
+ export const resolve = async (references, annotations = {}) => {
6
4
  const dependencies = {}
7
5
 
8
6
  for (const [dependency, components] of Object.entries(references)) {
9
- const { metadata, module } = load(dependency)
7
+ const { metadata, module } = await load(dependency)
10
8
  const id = metadata.name
11
9
 
12
10
  const instances = components.map((component) => ({
@@ -24,7 +22,26 @@ const resolve = (references, annotations) => {
24
22
  }
25
23
  }
26
24
 
25
+ for (const dependency of Object.keys(annotations)) {
26
+ if (dependency in dependencies) continue
27
+
28
+ // an annotation may be keyed by a dependency id rather than by a module reference
29
+ const module = await optional(dependency)
30
+
31
+ if (module?.standalone === true) dependencies[dependency] = []
32
+ }
33
+
27
34
  return dependencies
28
35
  }
29
36
 
30
- exports.resolve = resolve
37
+ /**
38
+ * @param {string} reference
39
+ * @returns {object | null}
40
+ */
41
+ async function optional(reference) {
42
+ try {
43
+ return (await load(reference)).module
44
+ } catch {
45
+ return null
46
+ }
47
+ }
@@ -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
@@ -1,13 +1,9 @@
1
- 'use strict'
1
+ import { connectors, extensions, resolve } from './.dependencies/index.js'
2
2
 
3
- const { connectors, extensions, resolve } = require('./.dependencies')
4
-
5
- const dependencies = async (context) => {
3
+ export const dependencies = async (context) => {
6
4
  const { extensions: e, components } = await extensions(context)
7
5
  const c = connectors(context, components)
8
6
  const references = { ...c, ...e }
9
7
 
10
8
  return resolve(references, context.annotations)
11
9
  }
12
-
13
- exports.dependencies = dependencies
@@ -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) => components[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
@@ -1,13 +1,15 @@
1
- 'use strict'
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
- exports.expand = expand
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
+ }
@@ -1,15 +1,6 @@
1
- 'use strict'
2
-
3
- const { complete } = require('./complete')
4
- const { dependencies } = require('./dependencies')
5
- const { dereference } = require('./dereference')
6
- const { expand } = require('./expand')
7
- const { normalize } = require('./normalize')
8
- const { validate } = require('./validate')
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 { dependencies } from './dependencies.js'
3
+ export { dereference } from './dereference.js'
4
+ export { expand } from './expand.js'
5
+ export { normalize } from './normalize.js'
6
+ export { validate } from './validate.js'
@@ -1,11 +1,9 @@
1
- 'use strict'
1
+ import * as runtime from '@toa.io/runtime'
2
2
 
3
3
  /**
4
4
  * @param {toa.norm.context.Declaration | Object} context
5
5
  */
6
- const normalize = (context) => {
7
- const runtime = require('@toa.io/runtime')
8
-
6
+ export const normalize = (context) => {
9
7
  if (context.runtime === undefined) context.runtime = { version: runtime.version }
10
8
  if (typeof context.runtime === 'string') context.runtime = { version: context.runtime }
11
9
 
@@ -14,6 +12,10 @@ const normalize = (context) => {
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
- exports.normalize = normalize
21
+ const PLATFORMS = ['linux/amd64', 'linux/arm/v7', 'linux/arm64']
@@ -6,12 +6,10 @@ properties:
6
6
  version:
7
7
  type: string
8
8
  name:
9
- $ref: 'definitions#/definitions/label'
10
- description:
11
9
  type: string
12
- packages:
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
- default: components/*
15
13
  build:
16
14
  type: object
17
15
  properties:
@@ -42,28 +40,151 @@ properties:
42
40
  minItems: 1
43
41
  items:
44
42
  type: string
45
- default:
46
- - linux/amd64
47
- - linux/arm/v7
48
- - linux/arm64
49
- required: [base, platforms]
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]
50
50
  compositions:
51
51
  type: array
52
52
  minItems: 1
53
53
  items:
54
54
  type: object
55
+ additionalProperties: false
55
56
  properties:
56
57
  name:
57
- $ref: 'definitions#/definitions/token'
58
+ type: string
59
+ pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
58
60
  image:
59
61
  type: string
60
62
  components:
61
63
  type: array
62
64
  minItems: 1
63
65
  items:
64
- $ref: 'definitions#/definitions/locator'
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'
65
80
  required: [name, components]
81
+ atomicity:
82
+ description: >
83
+ Where the replicas of a component decide together: who owns what, what they have spent
84
+ between them, and which of them holds a name while it works. Absent or unreachable,
85
+ nothing is owned and whoever asked stands down; metering and locking are refused.
86
+ type: object
87
+ additionalProperties: false
88
+ properties:
89
+ redis:
90
+ description: >
91
+ One address, or an odd number of them — independent servers, not the nodes of a
92
+ cluster. A lock is taken on a quorum of them; the registry and the meter use the
93
+ first, because each of them counts on a single key.
94
+ oneOf:
95
+ - type: string
96
+ - type: array
97
+ minItems: 1
98
+ items:
99
+ type: string
100
+ interval:
101
+ description: >
102
+ How often a replica registers, in milliseconds. It has to comfortably exceed a round
103
+ trip to Redis and the jitter around it, and nothing is owned until two consecutive
104
+ intervals have agreed — so this is also how long a replica owns nothing after it
105
+ joins, restarts or stalls.
106
+ type: integer
107
+ minimum: 100
108
+ required: [redis]
109
+ outbox:
110
+ type: object
111
+ additionalProperties: false
112
+ properties:
113
+ interval:
114
+ description: >
115
+ The pump's cycle, in milliseconds. One cycle reads what is unpublished, publishes it
116
+ and marks it. In steady state it reads nothing, so this is budgeted for how fast a rare
117
+ failure heals rather than for how fast events flow.
118
+ type: integer
119
+ minimum: 100
120
+ batch:
121
+ description: >
122
+ How many rows one read brings back. A page that comes back full is read on from,
123
+ so this bounds a read rather than a cycle.
124
+ type: integer
125
+ minimum: 1
126
+ retention:
127
+ description: >
128
+ Seconds a published row is kept as a change log before it expires. A row that never
129
+ made it out has no expiry and is never reaped.
130
+ type: integer
131
+ minimum: 0
132
+ events:
133
+ description: >
134
+ Events of this context that are consumed outside it. An event is otherwise published only
135
+ where something in the context receives it.
136
+ type: array
137
+ items:
138
+ type: string
139
+ pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$
140
+ mono:
141
+ type: object
142
+ additionalProperties: false
143
+ properties:
144
+ replicas:
145
+ type: integer
146
+ minimum: 1
147
+ resources:
148
+ $ref: '#/definitions/resources'
149
+ ingress:
150
+ type: object
151
+ additionalProperties: false
152
+ properties:
153
+ hosts:
154
+ type: array
155
+ minItems: 1
156
+ items:
157
+ type: string
158
+ class:
159
+ type: string
160
+ annotations:
161
+ type: object
162
+ default:
163
+ type: boolean
66
164
  annotations:
67
165
  type: object
166
+ resources:
167
+ $ref: '#/definitions/resources'
68
168
  required: [runtime, name, registry]
69
169
  additionalProperties: false
170
+
171
+ definitions:
172
+ # What a deployment may take. `null` deploys it without any, which is a decision
173
+ # rather than an omission — every deployment has to make one.
174
+ resources:
175
+ type: object
176
+ nullable: true
177
+ additionalProperties: false
178
+ properties:
179
+ cpu:
180
+ type: array
181
+ items:
182
+ type: string
183
+ minItems: 2
184
+ maxItems: 2
185
+ memory:
186
+ type: array
187
+ items:
188
+ type: string
189
+ minItems: 2
190
+ maxItems: 2