@toa.io/operations 1.0.0-alpha.27 → 1.0.0-alpha.270

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 (36) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/image/runtime.Dockerfile +4 -0
  3. package/package.json +8 -8
  4. package/readme.md +16 -0
  5. package/src/deployment/.deployment/.describe/compositions.js +7 -2
  6. package/src/deployment/.deployment/.describe/mounts.js +24 -0
  7. package/src/deployment/.deployment/.describe/resources.js +37 -0
  8. package/src/deployment/.deployment/.describe/services.js +28 -1
  9. package/src/deployment/.deployment/.describe/services.test.js +61 -0
  10. package/src/deployment/.deployment/.describe/variables.js +6 -5
  11. package/src/deployment/.deployment/describe.js +84 -6
  12. package/src/deployment/.deployment/merge.js +41 -1
  13. package/src/deployment/.deployment/merge.test.js +74 -0
  14. package/src/deployment/chart/templates/compositions.yaml +70 -0
  15. package/src/deployment/chart/templates/mono.yaml +177 -0
  16. package/src/deployment/chart/templates/services.yaml +71 -4
  17. package/src/deployment/chart/values.yaml +11 -1
  18. package/src/deployment/composition.js +1 -0
  19. package/src/deployment/deployment.js +6 -2
  20. package/src/deployment/factory.js +24 -8
  21. package/src/deployment/images/composition.Dockerfile +2 -2
  22. package/src/deployment/images/composition.js +16 -11
  23. package/src/deployment/images/factory.js +12 -3
  24. package/src/deployment/images/image.js +16 -7
  25. package/src/deployment/images/mono.Dockerfile +18 -0
  26. package/src/deployment/images/mono.js +71 -0
  27. package/src/deployment/images/runtime-base.test.js +90 -0
  28. package/src/deployment/images/service.Dockerfile +3 -3
  29. package/src/deployment/operator.js +4 -0
  30. package/src/deployment/registry.js +64 -16
  31. package/src/deployment/registry.test.js +175 -0
  32. package/types/_deployment/images/image.d.ts +4 -2
  33. package/types/_deployment/registry.d.ts +9 -7
  34. package/types/dependency.ts +69 -0
  35. package/types/dependency.d.ts +0 -39
  36. /package/types/{index.d.ts → index.ts} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,109 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **operations:** let a composition set its own base image ([61ba331](https://github.com/toa-io/toa/commit/61ba3319dc91915a3197d348111af411bbd5bff1))
12
+
13
+
14
+
15
+
16
+
17
+ # [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
18
+
19
+ **Note:** Version bump only for package @toa.io/operations
20
+
21
+
22
+
23
+
24
+
25
+ # [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
26
+
27
+ **Note:** Version bump only for package @toa.io/operations
28
+
29
+
30
+
31
+
32
+
33
+ # [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
34
+
35
+ **Note:** Version bump only for package @toa.io/operations
36
+
37
+
38
+
39
+
40
+
41
+ # [1.0.0-alpha.263](https://github.com/toa-io/toa/compare/v1.0.0-alpha.262...v1.0.0-alpha.263) (2026-08-29)
42
+
43
+ **Note:** Version bump only for package @toa.io/operations
44
+
45
+
46
+
47
+
48
+
49
+ # [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
50
+
51
+
52
+ ### Bug Fixes
53
+
54
+ * **cli:** boot toa mono from components and env, not context ([1518f72](https://github.com/toa-io/toa/commit/1518f729d7611ae88e0538f286231d283a7098ef))
55
+
56
+
57
+ * feat(operations)!: require service ports to be unique ([b22cf87](https://github.com/toa-io/toa/commit/b22cf8770d24c555b5f355a5f117a9043a57799b))
58
+
59
+
60
+ ### Features
61
+
62
+ * **operations:** let a service claim a path prefix on the host ([02677d1](https://github.com/toa-io/toa/commit/02677d17fcd4222b07c3be458d93bcc496a016a8))
63
+
64
+
65
+ ### BREAKING CHANGES
66
+
67
+ * an application whose own extension runs a service on a port
68
+ already taken by another — 8000 by the exposition gateway, 8001 by the telemetry
69
+ readiness probe — must move it.
70
+
71
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72
+
73
+
74
+
75
+
76
+
77
+ # [1.0.0-alpha.261](https://github.com/toa-io/toa/compare/v1.0.0-alpha.260...v1.0.0-alpha.261) (2026-08-26)
78
+
79
+
80
+ ### Bug Fixes
81
+
82
+ * **operations:** include extension pointer variables in --mono ([f8be23d](https://github.com/toa-io/toa/commit/f8be23d2aaf7bbe1b0580101c8bf4d32c3cab99f))
83
+
84
+
85
+
86
+
87
+
88
+ # [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
89
+
90
+ **Note:** Version bump only for package @toa.io/operations
91
+
92
+
93
+
94
+
95
+
96
+ # [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
97
+
98
+ **Note:** Version bump only for package @toa.io/operations
99
+
100
+
101
+
102
+
103
+
104
+ # [1.0.0-alpha.256](https://github.com/toa-io/toa/compare/v1.0.0-alpha.255...v1.0.0-alpha.256) (2026-08-23)
105
+
106
+
107
+ ### Features
108
+
109
+ * deploy a single mono image with toa deploy --mono ([a7f14f9](https://github.com/toa-io/toa/commit/a7f14f9877a280e9c74d16c195028b35d74fb15a))
@@ -0,0 +1,4 @@
1
+ FROM node:24.14.0-alpine3.22
2
+
3
+ ARG VERSION
4
+ RUN npm i -g @toa.io/runtime@${VERSION} --omit=dev
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/operations",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.270",
4
4
  "description": "Toa Deployment",
5
5
  "homepage": "https://toa.io",
6
6
  "author": {
@@ -16,23 +16,23 @@
16
16
  "url": "https://github.com/toa-io/toa/issues"
17
17
  },
18
18
  "engines": {
19
- "node": ">= 18.0.0"
19
+ "node": ">= 24.0.0"
20
20
  },
21
21
  "publishConfig": {
22
22
  "access": "public"
23
23
  },
24
24
  "main": "src/index.js",
25
- "types": "types/index.d.ts",
25
+ "types": "types/index.ts",
26
26
  "scripts": {
27
27
  "test": "echo \"Error: run tests from root\" && exit 1"
28
28
  },
29
29
  "dependencies": {
30
- "@toa.io/filesystem": "1.0.0-alpha.27",
31
- "@toa.io/generic": "1.0.0-alpha.27",
32
- "@toa.io/norm": "1.0.0-alpha.27",
33
- "@toa.io/yaml": "1.0.0-alpha.27",
30
+ "@toa.io/filesystem": "1.0.0-alpha.254",
31
+ "@toa.io/generic": "1.0.0-alpha.254",
32
+ "@toa.io/norm": "1.0.0-alpha.270",
33
+ "@toa.io/yaml": "1.0.0-alpha.254",
34
34
  "execa": "5.1.1",
35
35
  "fs-extra": "11.1.1"
36
36
  },
37
- "gitHead": "9bc4bdb919688c5272791020746f70d51a520750"
37
+ "gitHead": "94400bdd411b4c25074ffcf58184580ddd060268"
38
38
  }
package/readme.md CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  ### Container Registry
6
6
 
7
+ Deploy images default to `FROM ghcr.io/toa-io/runtime:<runtime.version>`
8
+ (published with each Toa release). The base image already has `@toa.io/runtime`
9
+ installed; composition/service Dockerfiles only install component dependencies.
10
+
11
+ To use a custom base image, set `registry.build.image` (or `composition.image`).
12
+ That image must provide the `toa` CLI, or install it via `registry.build.run`:
13
+
14
+ ```yaml
15
+ # context.toa.yaml
16
+
17
+ registry:
18
+ build:
19
+ image: node:24.14.0-alpine3.22
20
+ run: npm i -g @toa.io/runtime --omit=dev
21
+ ```
22
+
7
23
  #### Build Options
8
24
 
9
25
  ```yaml
@@ -1,10 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const { addVariables } = require('./variables')
4
+ const { addMounts } = require('./mounts')
4
5
 
5
- function compositions (compositions, variables) {
6
+ function compositions (compositions, dependency) {
6
7
  for (const composition of compositions) {
7
- addVariables(composition, variables)
8
+ addVariables(composition, dependency.variables)
9
+ addMounts(composition, dependency.mounts)
10
+
11
+ if (dependency.probe !== undefined && dependency.probe !== false)
12
+ composition.probe ??= dependency.probe
8
13
  }
9
14
  }
10
15
 
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ function addMounts (composition, mounts, keys = composition.components) {
4
+ if (mounts === undefined)
5
+ return
6
+
7
+ const used = new Set()
8
+
9
+ for (const [key, mount] of Object.entries(mounts)) {
10
+ if (key !== 'global' && !keys?.includes(key))
11
+ continue
12
+
13
+ for (const { name, path, claim } of mount) {
14
+ if (used.has(name))
15
+ continue
16
+
17
+ composition.mounts ??= []
18
+ composition.mounts.push({ name, path, claim })
19
+ used.add(name)
20
+ }
21
+ }
22
+ }
23
+
24
+ exports.addMounts = addMounts
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Every deployment states what it may take. One that states nothing is `BestEffort`: the
5
+ * first evicted under memory pressure and the last given CPU under contention — which on
6
+ * a busy node is slow enough for its own startup probe to kill it, in a loop, while the
7
+ * chart reports nothing wrong. That is not a thing to find out in production.
8
+ *
9
+ * A deployment takes its own declaration, or the context's `resources` where it has none.
10
+ * Deploying without any is a decision rather than an omission, and it is spelled
11
+ * `resources: null` — at either place.
12
+ */
13
+ function resources (context, values) {
14
+ for (const unit of units(values)) {
15
+ // `null` is an answer and `undefined` is not one, so the fallback reads only the latter
16
+ if (unit.deployment.resources === undefined)
17
+ unit.deployment.resources = context.resources
18
+
19
+ if (unit.deployment.resources === undefined)
20
+ throw new Error(`${unit.subject} declares no resources. ` +
21
+ 'Declare them on it or as the context\'s \'resources\', ' +
22
+ 'or \'resources: null\' to deploy it without any.')
23
+ }
24
+ }
25
+
26
+ function * units (values) {
27
+ if (values.mono !== undefined)
28
+ yield { deployment: values.mono, subject: 'The mono deployment' }
29
+
30
+ for (const composition of values.compositions ?? [])
31
+ yield { deployment: composition, subject: `Composition '${composition.name}'` }
32
+
33
+ for (const service of values.services ?? [])
34
+ yield { deployment: service, subject: `Service '${service.name}'` }
35
+ }
36
+
37
+ exports.resources = resources
@@ -2,10 +2,37 @@
2
2
 
3
3
  const { addVariables } = require('./variables')
4
4
 
5
- function services (services, variables) {
5
+ function services (services, variables, probe, ingress) {
6
6
  for (const service of services) {
7
7
  addVariables(service, variables)
8
+
9
+ if (service.probe === false)
10
+ delete service.probe
11
+ else if (service.probe === undefined && probe !== undefined && probe !== false)
12
+ service.probe = probe
13
+
14
+ if (service.ingress !== undefined)
15
+ expose(service, ingress)
8
16
  }
9
17
  }
10
18
 
19
+ /**
20
+ * A service declares only its own intent — a path, a port. Where that lands is
21
+ * the context's business, so everything else comes from its `ingress` section.
22
+ * What the service did declare wins.
23
+ */
24
+ function expose (service, ingress = {}) {
25
+ const declared = Object.fromEntries(
26
+ Object.entries(service.ingress).filter(([, value]) => value !== undefined))
27
+
28
+ service.ingress = Object.assign({}, ingress, declared)
29
+
30
+ if (service.ingress.hosts === undefined)
31
+ throw new Error(`Service '${service.name}' declares an ingress, but no hosts are defined. ` +
32
+ "Declare them in the context's 'ingress' section.")
33
+
34
+ if (service.port === undefined)
35
+ throw new Error(`Service '${service.name}' declares an ingress, but no port.`)
36
+ }
37
+
11
38
  exports.services = services
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ const { services } = require('./services')
4
+
5
+ const service = (extra = {}) => ({ group: 'group', name: 'group-one', version: '0', ...extra })
6
+
7
+ it('should leave a service without an ingress alone', () => {
8
+ const list = [service()]
9
+
10
+ services(list, {}, undefined, { hosts: ['api.dev'] })
11
+
12
+ expect(list[0].ingress).toBeUndefined()
13
+ })
14
+
15
+ /*
16
+ * A service declares only where it wants to land; the cluster-level plumbing
17
+ * belongs to the context.
18
+ */
19
+ it('should supply hosts, class and annotations from the context', () => {
20
+ const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
21
+
22
+ services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb', annotations: { a: 'b' } })
23
+
24
+ expect(list[0].ingress).toStrictEqual({
25
+ path: '/.introspection',
26
+ hosts: ['api.dev'],
27
+ class: 'alb',
28
+ annotations: { a: 'b' }
29
+ })
30
+ })
31
+
32
+ it('should keep what the service declared itself', () => {
33
+ const list = [service({ port: 8000, ingress: { path: '/', hosts: ['own.dev'] } })]
34
+
35
+ services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb' })
36
+
37
+ expect(list[0].ingress.hosts).toStrictEqual(['own.dev'])
38
+ expect(list[0].ingress.class).toStrictEqual('alb')
39
+ })
40
+
41
+ it('should ignore properties the service left undefined', () => {
42
+ const list = [service({ port: 8000, ingress: { path: '/', class: undefined } })]
43
+
44
+ services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb' })
45
+
46
+ expect(list[0].ingress.class).toStrictEqual('alb')
47
+ })
48
+
49
+ it('should reject an ingress with nowhere to land', () => {
50
+ const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
51
+
52
+ expect(() => services(list, {}, undefined, undefined))
53
+ .toThrow("Service 'group-one' declares an ingress, but no hosts are defined")
54
+ })
55
+
56
+ it('should reject an ingress without a port', () => {
57
+ const list = [service({ ingress: { path: '/.introspection' } })]
58
+
59
+ expect(() => services(list, {}, undefined, { hosts: ['api.dev'] }))
60
+ .toThrow("Service 'group-one' declares an ingress, but no port")
61
+ })
@@ -1,17 +1,18 @@
1
1
  'use strict'
2
2
 
3
- function addVariables (deployment, variables) {
4
- const used = new Set()
3
+ function addVariables (composition, variables, keys = composition.components) {
4
+ composition.variables ??= []
5
5
 
6
- deployment.variables ??= []
6
+ const used = new Set(composition.variables.map((variable) => variable.name))
7
7
 
8
8
  for (const [key, set] of Object.entries(variables)) {
9
- if (key !== 'global' && !deployment.components?.includes(key)) continue
9
+ if (key !== 'global' && !keys?.includes(key))
10
+ continue
10
11
 
11
12
  for (const variable of set) {
12
13
  if (used.has(variable.name)) continue
13
14
 
14
- deployment.variables.push(variable)
15
+ composition.variables.push(variable)
15
16
  used.add(variable.name)
16
17
  }
17
18
  }
@@ -1,8 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const get = require('./.describe')
3
+ const desc = require('./.describe')
4
+ const { addVariables } = require('./.describe/variables')
5
+ const { addMounts } = require('./.describe/mounts')
6
+ const { resources } = require('./.describe/resources')
4
7
 
5
- const describe = (context, compositions, dependency) => {
8
+ const describe = (context, compositions, dependency, image) => {
6
9
  const { services } = dependency
7
10
 
8
11
  dependency.variables.global ??= []
@@ -17,18 +20,93 @@ const describe = (context, compositions, dependency) => {
17
20
  }
18
21
  )
19
22
 
20
- const components = get.components(compositions)
21
23
  const credentials = context.registry?.credentials
22
24
 
23
- get.compositions(compositions, dependency.variables, context.environment)
24
- get.services(services, dependency.variables)
25
+ if (image !== undefined) {
26
+ const mono = unit(context, dependency)
25
27
 
26
- return {
28
+ mono.image = image.reference
29
+
30
+ const values = {
31
+ compositions: [],
32
+ components: mono.components,
33
+ services: [],
34
+ credentials,
35
+ mono
36
+ }
37
+
38
+ resources(context, values)
39
+
40
+ return values
41
+ }
42
+
43
+ const components = desc.components(compositions)
44
+
45
+ desc.compositions(compositions, dependency)
46
+ desc.services(services, dependency.variables, dependency.probe, context.ingress)
47
+
48
+ const values = {
27
49
  compositions,
28
50
  components,
29
51
  services,
30
52
  credentials
31
53
  }
54
+
55
+ resources(context, values)
56
+
57
+ return values
58
+ }
59
+
60
+ function unit (context, dependency) {
61
+ const components = (context.components ?? []).map((component) => component.locator.label)
62
+
63
+ const variables = dependency.variables ?? {}
64
+ const mounts = dependency.mounts ?? {}
65
+
66
+ const mono = {
67
+ replicas: context.mono?.replicas,
68
+ resources: context.mono?.resources,
69
+ components,
70
+ variables: []
71
+ }
72
+
73
+ if (context.ingress !== undefined)
74
+ mono.ingress = Object.assign({}, context.ingress)
75
+
76
+ addVariables(mono, variables, Object.keys(variables))
77
+ addMounts(mono, dependency.mounts, Object.keys(mounts))
78
+
79
+ for (const service of dependency.services ?? []) {
80
+ if (service.variables !== undefined)
81
+ for (const variable of service.variables)
82
+ if (!mono.variables.some((item) => item.name === variable.name))
83
+ mono.variables.push(variable)
84
+
85
+ // every declared port is bound by the single mono process, none is primary
86
+ if (service.port !== undefined)
87
+ (mono.backends ??= []).push({ port: service.port, path: service.ingress?.path ?? '/' })
88
+
89
+ if (service.ingress !== undefined) {
90
+ const { path, hosts, ...ingress } = service.ingress
91
+
92
+ mono.ingress = Object.assign(mono.ingress ?? {}, ingress)
93
+
94
+ // one Ingress serves every path, so its hosts are the union, not the last word
95
+ if (hosts !== undefined)
96
+ mono.ingress.hosts = [...new Set([...(mono.ingress.hosts ?? []), ...hosts])]
97
+ }
98
+
99
+ if (service.probe !== undefined && service.probe !== false)
100
+ mono.probe = service.probe
101
+ }
102
+
103
+ if (mono.probe === undefined && dependency.probe !== undefined && dependency.probe !== false)
104
+ mono.probe = dependency.probe
105
+
106
+ // the more specific prefix must come first, whatever the controller's tie-break
107
+ mono.backends?.sort((a, b) => b.path.length - a.path.length)
108
+
109
+ return mono
32
110
  }
33
111
 
34
112
  exports.describe = describe
@@ -13,14 +13,54 @@ const merge = (dependencies) => {
13
13
  /** @type {toa.deployment.dependency.Variables} */
14
14
  const variables = {}
15
15
 
16
+ const mounts = {}
17
+
18
+ /** @type {toa.deployment.dependency.Probe | false | undefined} */
19
+ let probe
20
+
16
21
  for (const dependency of dependencies) {
17
22
  if (dependency.references !== undefined) references.push(...dependency.references)
18
23
  if (dependency.services !== undefined) services.push(...dependency.services)
19
24
  if (dependency.proxies !== undefined) proxies.push(...dependency.proxies)
20
25
  if (dependency.variables !== undefined) append(variables, dependency.variables)
26
+ if (dependency.mounts !== undefined) append(mounts, dependency.mounts)
27
+ if (dependency.probe !== undefined) probe = dependency.probe
21
28
  }
22
29
 
23
- return { references, services, proxies, variables }
30
+ reserve(services, probe)
31
+
32
+ return { references, services, proxies, variables, mounts, probe }
33
+ }
34
+
35
+ /**
36
+ * In Kubernetes these are separate pods, but `toa mono` and a local run put every
37
+ * service in one process — so a port may be claimed once and only once.
38
+ */
39
+ const reserve = (services, probe) => {
40
+ const claimed = new Map()
41
+
42
+ if (probe !== undefined && probe !== false)
43
+ claimed.set(probe.port, 'the readiness probe')
44
+
45
+ for (const service of services)
46
+ for (const [port, claimant] of ports(service)) {
47
+ const conflicting = claimed.get(port)
48
+
49
+ if (conflicting !== undefined)
50
+ throw new Error(`Port ${port} is claimed by both ${conflicting} and ${claimant}`)
51
+
52
+ claimed.set(port, claimant)
53
+ }
54
+ }
55
+
56
+ function * ports (service) {
57
+ const name = `'${service.group}-${service.name}'`
58
+
59
+ if (service.port !== undefined)
60
+ yield [service.port, name]
61
+
62
+ if (service.probe !== undefined && service.probe !== false && service.probe.port !== service.port)
63
+ yield [service.probe.port, `the readiness probe of ${name}`]
24
64
  }
25
65
 
26
66
  const append = (merged, variables) => {
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ const { merge } = require('./merge')
4
+
5
+ const service = (name, extra = {}) => ({ group: 'group', name, version: '0', ...extra })
6
+
7
+ it('should merge services of all dependencies', () => {
8
+ const merged = merge([
9
+ { services: [service('one', { port: 8000 })] },
10
+ { services: [service('two', { port: 8001 })] }
11
+ ])
12
+
13
+ expect(merged.services).toHaveLength(2)
14
+ })
15
+
16
+ /*
17
+ * In Kubernetes these are separate pods, but `toa mono` and a local run put every
18
+ * service in one process.
19
+ */
20
+ describe('port reservation', () => {
21
+ it('should reject two services claiming one port', () => {
22
+ const dependencies = [
23
+ { services: [service('one', { port: 8000 })] },
24
+ { services: [service('two', { port: 8000 })] }
25
+ ]
26
+
27
+ expect(() => merge(dependencies))
28
+ .toThrow("Port 8000 is claimed by both 'group-one' and 'group-two'")
29
+ })
30
+
31
+ it('should reject a service claiming the port of the readiness probe', () => {
32
+ const dependencies = [
33
+ { probe: { path: '/.ready', port: 8001 } },
34
+ { services: [service('one', { port: 8001 })] }
35
+ ]
36
+
37
+ expect(() => merge(dependencies))
38
+ .toThrow("Port 8001 is claimed by both the readiness probe and 'group-one'")
39
+ })
40
+
41
+ it('should reject a probe claiming the port of another service', () => {
42
+ const dependencies = [
43
+ { services: [service('one', { port: 8000 })] },
44
+ { services: [service('two', { port: 8002, probe: { path: '/.ready', port: 8000 } })] }
45
+ ]
46
+
47
+ expect(() => merge(dependencies))
48
+ .toThrow("Port 8000 is claimed by both 'group-one' and the readiness probe of 'group-two'")
49
+ })
50
+
51
+ it('should allow a service to probe its own port', () => {
52
+ const dependencies = [
53
+ { services: [service('one', { port: 8000, probe: { path: '/.ready', port: 8000 } })] }
54
+ ]
55
+
56
+ expect(() => merge(dependencies)).not.toThrow()
57
+ })
58
+
59
+ it('should ignore services without a port', () => {
60
+ const dependencies = [
61
+ { services: [service('one'), service('two')] }
62
+ ]
63
+
64
+ expect(() => merge(dependencies)).not.toThrow()
65
+ })
66
+
67
+ it('should ignore a disabled probe', () => {
68
+ const dependencies = [
69
+ { probe: false, services: [service('one', { port: 8000, probe: false })] }
70
+ ]
71
+
72
+ expect(() => merge(dependencies)).not.toThrow()
73
+ })
74
+ })