@toa.io/operations 1.0.0-alpha.31 → 1.0.0-alpha.310

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 (76) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/image/runtime.Dockerfile +14 -0
  3. package/package.json +10 -10
  4. package/readme.md +201 -0
  5. package/src/deployment/.deployment/.describe/components.js +1 -5
  6. package/src/deployment/.deployment/.describe/compositions.js +26 -7
  7. package/src/deployment/.deployment/.describe/dependencies.js +1 -6
  8. package/src/deployment/.deployment/.describe/events.js +63 -0
  9. package/src/deployment/.deployment/.describe/fold.js +42 -0
  10. package/src/deployment/.deployment/.describe/fold.test.js +108 -0
  11. package/src/deployment/.deployment/.describe/index.js +4 -13
  12. package/src/deployment/.deployment/.describe/mounts.js +17 -0
  13. package/src/deployment/.deployment/.describe/resources.js +129 -0
  14. package/src/deployment/.deployment/.describe/resources.test.js +129 -0
  15. package/src/deployment/.deployment/.describe/services.js +32 -6
  16. package/src/deployment/.deployment/.describe/services.test.js +78 -0
  17. package/src/deployment/.deployment/.describe/variables.js +5 -9
  18. package/src/deployment/.deployment/declare.js +1 -5
  19. package/src/deployment/.deployment/describe.js +153 -10
  20. package/src/deployment/.deployment/index.js +3 -9
  21. package/src/deployment/.deployment/merge.js +66 -6
  22. package/src/deployment/.deployment/merge.test.js +133 -0
  23. package/src/deployment/chart/templates/components.configmap.yaml +14 -0
  24. package/src/deployment/chart/templates/components.yaml +1 -1
  25. package/src/deployment/chart/templates/compositions.yaml +109 -4
  26. package/src/deployment/chart/templates/mono.yaml +195 -0
  27. package/src/deployment/chart/templates/services.yaml +97 -7
  28. package/src/deployment/chart/values.yaml +23 -1
  29. package/src/deployment/composition.js +8 -6
  30. package/src/deployment/deployment.js +147 -34
  31. package/src/deployment/drain.js +70 -0
  32. package/src/deployment/drain.test.js +64 -0
  33. package/src/deployment/factory.js +169 -37
  34. package/src/deployment/images/bundle.js +88 -0
  35. package/src/deployment/images/composition.Dockerfile +9 -17
  36. package/src/deployment/images/composition.js +9 -56
  37. package/src/deployment/images/dependencies.Dockerfile +23 -0
  38. package/src/deployment/images/dependencies.js +171 -0
  39. package/src/deployment/images/dependencies.test.js +268 -0
  40. package/src/deployment/images/factory.js +31 -15
  41. package/src/deployment/images/format.js +66 -0
  42. package/src/deployment/images/format.test.js +149 -0
  43. package/src/deployment/images/image.fixtures.js +17 -19
  44. package/src/deployment/images/image.js +79 -38
  45. package/src/deployment/images/image.test.js +11 -5
  46. package/src/deployment/images/index.js +1 -5
  47. package/src/deployment/images/mono.Dockerfile +11 -0
  48. package/src/deployment/images/mono.js +15 -0
  49. package/src/deployment/images/publish.js +91 -0
  50. package/src/deployment/images/runtime-base.test.js +97 -0
  51. package/src/deployment/images/service.Dockerfile +17 -5
  52. package/src/deployment/images/service.js +63 -14
  53. package/src/deployment/index.js +1 -5
  54. package/src/deployment/operator.js +22 -16
  55. package/src/deployment/operator.test.js +57 -7
  56. package/src/deployment/registry.js +210 -46
  57. package/src/deployment/registry.test.js +539 -0
  58. package/src/deployment/service.js +4 -6
  59. package/src/deployment/workspace.js +13 -8
  60. package/src/index.js +3 -2
  61. package/src/process.js +51 -13
  62. package/src/process.test.js +33 -0
  63. package/types/_deployment/composition.d.ts +2 -3
  64. package/types/_deployment/dependency.d.ts +13 -7
  65. package/types/_deployment/deployment.d.ts +7 -8
  66. package/types/_deployment/factory.d.ts +2 -4
  67. package/types/_deployment/images/factory.d.ts +6 -8
  68. package/types/_deployment/images/image.d.ts +16 -1
  69. package/types/_deployment/images/registry.d.ts +8 -11
  70. package/types/_deployment/operator.d.ts +10 -9
  71. package/types/_deployment/registry.d.ts +7 -4
  72. package/types/_deployment/service.d.ts +4 -3
  73. package/types/dependency.ts +79 -0
  74. package/types/index.ts +1 -0
  75. package/types/dependency.d.ts +0 -39
  76. package/types/index.d.ts +0 -1
@@ -0,0 +1,195 @@
1
+ {{- if .Values.mono }}
2
+ {{- with .Values.mono }}
3
+ apiVersion: apps/v1
4
+ kind: Deployment
5
+ metadata:
6
+ name: mono
7
+ spec:
8
+ replicas: {{ .replicas | default 2 }}
9
+ progressDeadlineSeconds: 900
10
+ strategy:
11
+ type: RollingUpdate
12
+ rollingUpdate:
13
+ maxUnavailable: 0
14
+ maxSurge: 100%
15
+ selector:
16
+ matchLabels:
17
+ toa/composition: mono
18
+ template:
19
+ metadata:
20
+ labels:
21
+ toa/composition: mono
22
+ {{- range .components }}
23
+ toa/component-{{ . }}: "1"
24
+ {{- end }}
25
+ spec:
26
+ containers:
27
+ - name: mono
28
+ image: {{ .image }}
29
+ {{- if or .backends .probe }}
30
+ ports:
31
+ {{- range .backends }}
32
+ - containerPort: {{ .port }}
33
+ {{- end }}
34
+ {{- if .probe }}
35
+ - containerPort: {{ .probe.port }}
36
+ {{- end }}
37
+ {{- end }}
38
+ {{- if .resources }}
39
+ resources:
40
+ {{- if or .resources.cpu .resources.memory }}
41
+ requests:
42
+ {{- if .resources.cpu }}
43
+ cpu: {{ index .resources.cpu 0 }}
44
+ {{- end }}
45
+ {{- if .resources.memory }}
46
+ memory: {{ index .resources.memory 0 }}
47
+ {{- end }}
48
+ limits:
49
+ {{- if .resources.cpu }}
50
+ cpu: {{ index .resources.cpu 1 }}
51
+ {{- end }}
52
+ {{- if .resources.memory }}
53
+ memory: {{ index .resources.memory 1 }}
54
+ {{- end }}
55
+ {{- end }}
56
+ {{- end }}
57
+ {{- if .variables }}
58
+ env:
59
+ {{- range .variables }}
60
+ {{- include "env.var" . | indent 12 }}
61
+ {{- end }}
62
+ {{- end }}
63
+ {{- if .probe }}
64
+ startupProbe:
65
+ httpGet:
66
+ path: {{ .probe.path }}
67
+ port: {{ .probe.port }}
68
+ {{- if .probe.delay }}
69
+ initialDelaySeconds: {{ .probe.delay }}
70
+ {{- end }}
71
+ periodSeconds: 2
72
+ timeoutSeconds: 3
73
+ failureThreshold: 150
74
+ readinessProbe:
75
+ httpGet:
76
+ path: {{ .probe.path }}
77
+ port: {{ .probe.port }}
78
+ # a replica is ready when it says so, not at the next tick of a slow clock
79
+ periodSeconds: 2
80
+ timeoutSeconds: 3
81
+ failureThreshold: 5
82
+ {{- end }}
83
+ {{- if or .mounts $.Values.map }}
84
+ volumeMounts:
85
+ {{- range .mounts }}
86
+ - name: {{ .name }}
87
+ mountPath: {{ .path }}
88
+ {{- end }}
89
+ {{- if $.Values.map }}
90
+ - name: components
91
+ mountPath: {{ $.Values.map.directory }}
92
+ readOnly: true
93
+ {{- end }}
94
+ {{- end }}
95
+ lifecycle:
96
+ preStop:
97
+ exec:
98
+ command: [sleep, "5"]
99
+ terminationGracePeriodSeconds: 45
100
+ {{- if $.Values.credentials }}
101
+ imagePullSecrets:
102
+ - name: {{ $.Values.credentials }}
103
+ {{- end }}
104
+ {{- if or .mounts $.Values.map }}
105
+ volumes:
106
+ {{- range .mounts }}
107
+ - name: {{ .name }}
108
+ persistentVolumeClaim:
109
+ claimName: {{ .claim }}
110
+ {{- end }}
111
+ {{- if $.Values.map }}
112
+ - name: components
113
+ configMap:
114
+ name: components
115
+ {{- end }}
116
+ {{- end }}
117
+ topologySpreadConstraints:
118
+ - maxSkew: 1
119
+ topologyKey: kubernetes.io/hostname
120
+ whenUnsatisfiable: ScheduleAnyway
121
+ labelSelector:
122
+ matchLabels:
123
+ toa/composition: mono
124
+ # only this revision's pods: a rollout spreads the new ones, whatever the old ones occupy
125
+ matchLabelKeys:
126
+ - pod-template-hash
127
+ {{- if .backends }}
128
+ ---
129
+ apiVersion: v1
130
+ kind: Service
131
+ metadata:
132
+ name: mono
133
+ {{- if .annotations }}
134
+ annotations:
135
+ {{ toYaml .annotations | indent 4 }}
136
+ {{- end }}
137
+ spec:
138
+ type: ClusterIP
139
+ selector:
140
+ toa/composition: mono
141
+ ports:
142
+ {{- range .backends }}
143
+ - name: port-{{ .port }}
144
+ protocol: TCP
145
+ port: {{ .port }}
146
+ targetPort: {{ .port }}
147
+ {{- end }}
148
+ {{- end }}
149
+ {{- if .ingress }}
150
+ ---
151
+ apiVersion: networking.k8s.io/v1
152
+ kind: Ingress
153
+ metadata:
154
+ name: mono
155
+ {{- if .ingress.annotations }}
156
+ annotations:
157
+ {{ toYaml .ingress.annotations | indent 4 }}
158
+ {{- end }}
159
+ spec:
160
+ {{- if .ingress.class }}
161
+ ingressClassName: {{ .ingress.class }}
162
+ {{- end }}
163
+ {{- $backends := .backends }}
164
+ rules:
165
+ {{- range .ingress.hosts }}
166
+ - host: {{ . }}
167
+ http:
168
+ paths:
169
+ {{- range $backends }}
170
+ - path: {{ .path }}
171
+ pathType: Prefix
172
+ backend:
173
+ service:
174
+ name: mono
175
+ port:
176
+ number: {{ .port }}
177
+ {{- end }}
178
+ {{- end }}
179
+ {{- if .ingress.default }}
180
+ - http:
181
+ paths:
182
+ {{- range $backends }}
183
+ - path: {{ .path }}
184
+ pathType: Prefix
185
+ backend:
186
+ service:
187
+ name: mono
188
+ port:
189
+ number: {{ .port }}
190
+ {{- end }}
191
+ {{- end }}
192
+ {{- end }}
193
+ ---
194
+ {{- end }}
195
+ {{- end }}
@@ -1,21 +1,52 @@
1
1
  {{- range .Values.services }}
2
+ {{- if not .workload }}
2
3
  apiVersion: apps/v1
3
4
  kind: Deployment
4
5
  metadata:
5
6
  name: extension-{{ required "deployment name is required" .name }}
6
7
  spec:
7
8
  replicas: {{ .replicas | default 2 }}
9
+ # A backstop for stuck rollouts only. Must stay above the `helm upgrade --timeout`
10
+ # the deploy runs with, so a slow but progressing rollout is not marked failed.
11
+ progressDeadlineSeconds: 900
12
+ strategy:
13
+ type: RollingUpdate
14
+ rollingUpdate:
15
+ # a replacement must pass readiness before a running replica is taken out, and every
16
+ # replacement starts at once: the rollout takes one start-up, not one per replica
17
+ maxUnavailable: 0
18
+ maxSurge: 100%
8
19
  selector:
9
20
  matchLabels:
10
- toa.io/service: extension-{{ .name }}
21
+ toa/service: extension-{{ .name }}
11
22
  template:
12
23
  metadata:
13
24
  labels:
14
- toa.io/service: extension-{{ .name }}
25
+ toa/service: extension-{{ .name }}
26
+ toa/service-{{ .name }}: "1"
15
27
  spec:
16
28
  containers:
17
29
  - name: extension-{{ .name }}
18
30
  image: {{ .image }}
31
+ {{- if .resources }}
32
+ resources:
33
+ {{- if or .resources.cpu .resources.memory }}
34
+ requests:
35
+ {{- if .resources.cpu }}
36
+ cpu: {{ index .resources.cpu 0 }}
37
+ {{- end }}
38
+ {{- if .resources.memory }}
39
+ memory: {{ index .resources.memory 0 }}
40
+ {{- end }}
41
+ limits:
42
+ {{- if .resources.cpu }}
43
+ cpu: {{ index .resources.cpu 1 }}
44
+ {{- end }}
45
+ {{- if .resources.memory }}
46
+ memory: {{ index .resources.memory 1 }}
47
+ {{- end }}
48
+ {{- end }}
49
+ {{- end }}
19
50
  {{- if .variables }}
20
51
  env:
21
52
  {{- range .variables }}
@@ -23,31 +54,78 @@ spec:
23
54
  {{- end }}
24
55
  {{- end }}
25
56
  {{- if .probe }}
26
- readinessProbe:
57
+ startupProbe:
27
58
  httpGet:
28
59
  path: {{ .probe.path }}
29
60
  port: {{ .probe.port }}
30
61
  {{- if .probe.delay }}
31
62
  initialDelaySeconds: {{ .probe.delay }}
32
63
  {{- end }}
64
+ periodSeconds: 2
65
+ timeoutSeconds: 3
66
+ failureThreshold: 150
67
+ readinessProbe:
68
+ httpGet:
69
+ path: {{ .probe.path }}
70
+ port: {{ .probe.port }}
71
+ # a replica is ready when it says so, not at the next tick of a slow clock
72
+ periodSeconds: 2
73
+ timeoutSeconds: 3
74
+ failureThreshold: 5
75
+ {{- end }}
76
+ {{- if $.Values.map }}
77
+ volumeMounts:
78
+ - name: components
79
+ mountPath: {{ $.Values.map.directory }}
80
+ readOnly: true
33
81
  {{- end }}
82
+ lifecycle:
83
+ preStop:
84
+ exec:
85
+ command: [sleep, "5"]
86
+ {{- if $.Values.map }}
87
+ volumes:
88
+ - name: components
89
+ configMap:
90
+ name: components
91
+ {{- end }}
92
+ terminationGracePeriodSeconds: 45
93
+ topologySpreadConstraints:
94
+ - maxSkew: 1
95
+ topologyKey: kubernetes.io/hostname
96
+ whenUnsatisfiable: ScheduleAnyway
97
+ labelSelector:
98
+ matchLabels:
99
+ toa/service: extension-{{ .name }}
100
+ # only this revision's pods: a rollout spreads the new ones, whatever the old ones occupy
101
+ matchLabelKeys:
102
+ - pod-template-hash
103
+ {{- end }}
104
+ {{- if .port }}
34
105
  ---
35
106
  apiVersion: v1
36
107
  kind: Service
37
108
  metadata:
38
109
  name: extension-{{ .name }}
110
+ {{- if .annotations }}
111
+ annotations:
112
+ {{ toYaml .annotations | indent 4 }}
113
+ {{- end }}
39
114
  spec:
40
115
  type: ClusterIP
41
116
  selector:
42
- toa.io/service: extension-{{ .name }}
117
+ toa/service-{{ .name }}: "1"
43
118
  ports:
44
119
  - name: port-{{ .port }}
45
120
  protocol: TCP
46
121
  port: {{ .port }}
47
122
  targetPort: {{ .port }}
48
- ---
123
+ {{- end }}
49
124
  {{- if .ingress }}
50
125
  {{- $service := .name }}
126
+ {{- $port := .port }}
127
+ {{- $path := .ingress.path | default "/" }}
128
+ ---
51
129
  apiVersion: networking.k8s.io/v1
52
130
  kind: Ingress
53
131
  metadata:
@@ -65,13 +143,25 @@ spec:
65
143
  - host: {{ . }}
66
144
  http:
67
145
  paths:
68
- - path: /
146
+ - path: {{ $path }}
69
147
  pathType: Prefix
70
148
  backend:
71
149
  service:
72
150
  name: extension-{{ $service }}
73
151
  port:
74
- number: 8000
152
+ number: {{ $port }}
153
+ {{- end }}
154
+ {{- if .ingress.default }}
155
+ - http:
156
+ paths:
157
+ - path: {{ $path }}
158
+ pathType: Prefix
159
+ backend:
160
+ service:
161
+ name: extension-{{ $service }}
162
+ port:
163
+ number: {{ $port }}
75
164
  {{- end }}
76
165
  {{- end }}
166
+ ---
77
167
  {{- end }}
@@ -3,9 +3,16 @@ compositions:
3
3
  - name: todos
4
4
  image: localhost:5000/composition-todos:0.0.0
5
5
  replicas: 2
6
+ mounts:
7
+ - name: mount-name
8
+ path: /storage
9
+ claim: storage-pvc
6
10
  components:
7
11
  - todos-tasks
8
12
  - todos-stats
13
+ resources:
14
+ cpu: [100m, 1]
15
+ memory: [100Mi, 1Gi]
9
16
  variables:
10
17
  - name: TOA_CONFIGURATION_TODOS_TASKS
11
18
  value: foo
@@ -18,17 +25,32 @@ compositions:
18
25
  replicas: 3
19
26
  components:
20
27
  - users-users
21
- # TODO: create component services only if sync binding is being used
28
+ # the extensions whose services this composition runs in its own pod
29
+ services:
30
+ - '@toa.io/extensions.exposition'
31
+ # each of those, as its own Service selects it
32
+ hosted:
33
+ - exposition-gateway
34
+ # the ports they bind
35
+ backends:
36
+ - port: 8000
37
+ path: /
22
38
  components:
23
39
  - todos-tasks
24
40
  - todos-stats
25
41
  - users-users
26
42
  services:
43
+ # a service a composition runs carries `workload`, and gets no deployment of its own —
44
+ # its Service and Ingress stay, and select that composition's pods
27
45
  - name: resources-gateway
28
46
  image: localhost:5000/resources-gateway:0.0.0
29
47
  port: 8000
30
48
  replicas: 2
49
+ resources:
50
+ cpu: [100m, 1]
51
+ memory: [100Mi, 1Gi]
31
52
  ingress:
53
+ default: true
32
54
  hosts: [dummies.toa.io]
33
55
  class: alb
34
56
  annotations:
@@ -1,18 +1,20 @@
1
- 'use strict'
2
-
3
- class Composition {
1
+ export class Composition {
4
2
  name
5
3
  image
6
4
  /** @type {string[]} */
7
5
  components
8
6
 
9
- constructor (composition, image) {
7
+ constructor(composition, image) {
10
8
  this.name = composition.name
11
9
  this.image = image.reference
12
10
  this.components = composition.components.map(component)
11
+ this.resources = composition.resources
12
+
13
+ if (composition.replicas !== undefined) this.replicas = composition.replicas
14
+
15
+ // the extensions whose services this composition runs, as `TOA_SERVICES` for the process
16
+ if (composition.services !== undefined) this.services = composition.services
13
17
  }
14
18
  }
15
19
 
16
20
  const component = (component) => component.locator.label
17
-
18
- exports.Composition = Composition
@@ -1,90 +1,203 @@
1
- 'use strict'
1
+ import { join } from 'node:path'
2
+ import { writeFile as write } from 'node:fs/promises'
3
+ import { yaml as jsyaml } from '@toa.io/generic'
4
+ import { cp } from 'node:fs/promises'
5
+ import { shortcuts } from '@toa.io/norm'
2
6
 
3
- const { join } = require('node:path')
4
- const { writeFile: write } = require('node:fs/promises')
5
- const { dump } = require('@toa.io/yaml')
6
- const fs = require('fs-extra')
7
+ import { merge, declare, describe } from './.deployment/index.js'
8
+ import { drain } from './drain.js'
7
9
 
8
- const { merge, declare, describe } = require('./.deployment')
9
-
10
- class Deployment {
10
+ export class Deployment {
11
11
  #chart
12
12
  #values
13
+ #keyed
13
14
  #process
14
15
  #target
15
16
 
16
- constructor (context, compositions, dependencies, process) {
17
+ constructor(context, compositions, dependencies, process, image) {
17
18
  const dependency = merge(dependencies)
18
19
 
19
20
  this.#chart = declare(context, dependency)
20
- this.#values = describe(context, compositions, dependency)
21
+ this.#values = describe(context, compositions, dependency, image)
22
+ this.#keyed = dependency.variables
21
23
  this.#process = process
22
24
  }
23
25
 
24
- async export (target) {
26
+ async export(target) {
25
27
  const chart = dump(this.#chart)
26
28
  const values = dump(this.#values)
27
29
 
28
30
  await Promise.all([
29
31
  write(join(target, 'Chart.yaml'), chart),
30
32
  write(join(target, 'values.yaml'), values),
31
- fs.copy(TEMPLATES, join(target, 'templates'))
33
+ cp(TEMPLATES, join(target, 'templates'), { recursive: true })
32
34
  ])
33
35
 
34
36
  this.#target = target
35
37
  }
36
38
 
37
- async install (options) {
39
+ async install(options) {
38
40
  if (options.target) this.#target = options.target
39
- if (this.#target === undefined) throw new Error('Deployment hasn\'t been exported')
41
+ if (this.#target === undefined) throw new Error("Deployment hasn't been exported")
40
42
 
41
43
  const args = []
42
44
 
43
45
  if (options.namespace !== undefined) args.push('-n', options.namespace)
44
46
  if (options.wait === true) args.push('--wait')
47
+ if (options.timeout !== undefined) args.push('--timeout', options.timeout)
48
+
49
+ await this.#update()
50
+ await this.#process.execute('helm', [
51
+ 'upgrade',
52
+ this.#chart.name,
53
+ '-i',
54
+ ...args,
55
+ this.#target
56
+ ])
45
57
 
46
- await this.#process.execute('helm', ['dependency', 'update', this.#target])
47
- await this.#process.execute('helm', ['upgrade', this.#chart.name, '-i', ...args, this.#target])
58
+ // ready is not done: the replicas replaced are still draining when helm answers
59
+ if (options.wait === true) await drain(this.#process, options)
48
60
  }
49
61
 
50
- async template (options) {
51
- if (this.#target === undefined) throw new Error('Deployment hasn\'t been exported')
62
+ async template(options) {
63
+ if (this.#target === undefined) throw new Error("Deployment hasn't been exported")
52
64
 
53
- await this.#process.execute('helm', ['dependency', 'update', this.#target], { silently: true })
65
+ await this.#update({ silently: true })
54
66
 
55
67
  const args = []
56
68
 
57
69
  if (options.namespace !== undefined) args.push('-n', options.namespace)
58
70
 
59
- return await this.#process.execute('helm',
71
+ return await this.#process.execute(
72
+ 'helm',
60
73
  ['template', this.#chart.name, ...args, this.#target],
61
- { silently: true })
74
+ { silently: true }
75
+ )
76
+ }
77
+
78
+ /**
79
+ * A chart with no subcharts has nothing to resolve, and the call still reaches out for
80
+ * the repositories it would have read.
81
+ *
82
+ * @param {object} [options]
83
+ * @returns {Promise<void>}
84
+ */
85
+ async #update(options = {}) {
86
+ if (this.#chart.dependencies.length === 0) return
87
+
88
+ await this.#process.execute('helm', ['dependency', 'update', this.#target], options)
89
+ }
90
+
91
+ /**
92
+ * @param {{ components?: string[], services?: string[] }} [options]
93
+ * @returns {toa.deployment.dependency.Variable[]}
94
+ */
95
+ variables(options = {}) {
96
+ if (options.components === undefined && options.services === undefined) {
97
+ const variables = []
98
+ const used = new Set()
99
+
100
+ addVariables(this.#values.compositions, variables, used)
101
+ addVariables(this.#values.services, variables, used)
102
+
103
+ if (this.#values.mono !== undefined)
104
+ addVariables([this.#values.mono], variables, used)
105
+
106
+ return variables
107
+ }
108
+
109
+ return this.#select(options.components, options.services)
62
110
  }
63
111
 
64
- variables () {
112
+ /**
113
+ * @param {string[] | undefined} components
114
+ * @param {string[] | undefined} services
115
+ * @returns {toa.deployment.dependency.Variable[]}
116
+ */
117
+ #select(components, services) {
118
+ const labels = labelsOf(this.#values, this.#keyed)
119
+ const keys = new Set(['global'])
120
+ const selected = []
121
+
122
+ for (const token of components ?? []) keys.add(matchComponent(token, labels))
123
+
124
+ for (const token of services ?? []) {
125
+ const service = matchService(token, this.#values.services)
126
+
127
+ selected.push(service)
128
+
129
+ for (const label of service.components ?? []) keys.add(label)
130
+ }
131
+
65
132
  const variables = []
66
133
  const used = new Set()
67
134
 
68
- addVariables(this.#values.compositions, variables, used)
69
- addVariables(this.#values.services, variables, used)
135
+ for (const key of keys) append(this.#keyed[key], variables, used)
136
+
137
+ for (const service of selected) append(service.variables, variables, used)
70
138
 
71
139
  return variables
72
140
  }
73
141
  }
74
142
 
75
- function addVariables (list, variables, used = new Set()) {
76
- for (const item of list) {
77
- if (item.variables === undefined) continue
143
+ function addVariables(list, variables, used = new Set()) {
144
+ if (list === undefined) return
78
145
 
79
- for (const variable of item.variables) {
80
- if (used.has(variable.name)) continue
146
+ for (const item of list) append(item.variables, variables, used)
147
+ }
81
148
 
82
- variables.push(variable)
83
- used.add(variable.name)
84
- }
149
+ function append(set, variables, used) {
150
+ if (set === undefined) return
151
+
152
+ for (const variable of set) {
153
+ if (used.has(variable.name)) continue
154
+
155
+ variables.push(variable)
156
+ used.add(variable.name)
85
157
  }
86
158
  }
87
159
 
88
- const TEMPLATES = join(__dirname, 'chart/templates')
160
+ function labelsOf(values, keyed) {
161
+ const labels = new Set(values.components ?? [])
162
+
163
+ for (const service of values.services ?? [])
164
+ for (const label of service.components ?? []) labels.add(label)
165
+
166
+ for (const key of Object.keys(keyed ?? {})) if (key !== 'global') labels.add(key)
167
+
168
+ return labels
169
+ }
170
+
171
+ function matchComponent(token, labels) {
172
+ const candidates = [token, token.replaceAll('.', '-').toLowerCase()]
173
+
174
+ if (!token.includes('.')) candidates.push('default-' + token.toLowerCase())
175
+
176
+ for (const candidate of candidates) if (labels.has(candidate)) return candidate
177
+
178
+ throw new Error(`Component '${token}' is not in the context`)
179
+ }
180
+
181
+ function matchService(token, services) {
182
+ const resolved = shortcuts.resolve(token)
183
+
184
+ const service = (services ?? []).find((service) => {
185
+ if (service.name === token || service.group === token) return true
186
+
187
+ for (const [alias, pkg] of Object.entries(shortcuts.SHORTCUTS))
188
+ if (resolved === pkg && service.group === alias) return true
189
+
190
+ return false
191
+ })
192
+
193
+ if (service === undefined) throw new Error(`Service '${token}' is not in the context`)
194
+
195
+ return service
196
+ }
197
+
198
+ const TEMPLATES = join(import.meta.dirname, 'chart/templates')
89
199
 
90
- exports.Deployment = Deployment
200
+ function dump(object) {
201
+ // js-yaml writes plain objects only, and the values carry locators and images
202
+ return jsyaml.dump(JSON.parse(JSON.stringify(object)), { noRefs: true, lineWidth: -1 })
203
+ }