@toa.io/operations 1.0.0-alpha.30 → 1.0.0-alpha.301

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 (75) hide show
  1. package/CHANGELOG.md +142 -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 +138 -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.yaml +1 -1
  24. package/src/deployment/chart/templates/compositions.yaml +99 -4
  25. package/src/deployment/chart/templates/mono.yaml +185 -0
  26. package/src/deployment/chart/templates/services.yaml +85 -7
  27. package/src/deployment/chart/values.yaml +23 -1
  28. package/src/deployment/composition.js +6 -6
  29. package/src/deployment/deployment.js +147 -34
  30. package/src/deployment/drain.js +70 -0
  31. package/src/deployment/drain.test.js +64 -0
  32. package/src/deployment/factory.js +169 -37
  33. package/src/deployment/images/bundle.js +88 -0
  34. package/src/deployment/images/composition.Dockerfile +8 -16
  35. package/src/deployment/images/composition.js +9 -56
  36. package/src/deployment/images/dependencies.Dockerfile +23 -0
  37. package/src/deployment/images/dependencies.js +171 -0
  38. package/src/deployment/images/dependencies.test.js +268 -0
  39. package/src/deployment/images/factory.js +31 -15
  40. package/src/deployment/images/format.js +66 -0
  41. package/src/deployment/images/format.test.js +149 -0
  42. package/src/deployment/images/image.fixtures.js +17 -19
  43. package/src/deployment/images/image.js +77 -38
  44. package/src/deployment/images/image.test.js +11 -5
  45. package/src/deployment/images/index.js +1 -5
  46. package/src/deployment/images/mono.Dockerfile +11 -0
  47. package/src/deployment/images/mono.js +15 -0
  48. package/src/deployment/images/publish.js +91 -0
  49. package/src/deployment/images/runtime-base.test.js +97 -0
  50. package/src/deployment/images/service.Dockerfile +16 -4
  51. package/src/deployment/images/service.js +63 -14
  52. package/src/deployment/index.js +1 -5
  53. package/src/deployment/operator.js +22 -16
  54. package/src/deployment/operator.test.js +57 -7
  55. package/src/deployment/registry.js +210 -46
  56. package/src/deployment/registry.test.js +539 -0
  57. package/src/deployment/service.js +4 -6
  58. package/src/deployment/workspace.js +13 -8
  59. package/src/index.js +3 -2
  60. package/src/process.js +51 -13
  61. package/src/process.test.js +33 -0
  62. package/types/_deployment/composition.d.ts +1 -3
  63. package/types/_deployment/dependency.d.ts +13 -7
  64. package/types/_deployment/deployment.d.ts +7 -8
  65. package/types/_deployment/factory.d.ts +2 -4
  66. package/types/_deployment/images/factory.d.ts +6 -8
  67. package/types/_deployment/images/image.d.ts +16 -1
  68. package/types/_deployment/images/registry.d.ts +8 -11
  69. package/types/_deployment/operator.d.ts +10 -9
  70. package/types/_deployment/registry.d.ts +7 -4
  71. package/types/_deployment/service.d.ts +4 -3
  72. package/types/dependency.ts +79 -0
  73. package/types/index.ts +1 -0
  74. package/types/dependency.d.ts +0 -39
  75. package/types/index.d.ts +0 -1
@@ -0,0 +1,185 @@
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 .mounts }}
84
+ volumeMounts:
85
+ {{- range .mounts }}
86
+ - name: {{ .name }}
87
+ mountPath: {{ .path }}
88
+ {{- end }}
89
+ {{- end }}
90
+ lifecycle:
91
+ preStop:
92
+ exec:
93
+ command: [sleep, "5"]
94
+ terminationGracePeriodSeconds: 45
95
+ {{- if $.Values.credentials }}
96
+ imagePullSecrets:
97
+ - name: {{ $.Values.credentials }}
98
+ {{- end }}
99
+ {{- if .mounts }}
100
+ volumes:
101
+ {{- range .mounts }}
102
+ - name: {{ .name }}
103
+ persistentVolumeClaim:
104
+ claimName: {{ .claim }}
105
+ {{- end }}
106
+ {{- end }}
107
+ topologySpreadConstraints:
108
+ - maxSkew: 1
109
+ topologyKey: kubernetes.io/hostname
110
+ whenUnsatisfiable: ScheduleAnyway
111
+ labelSelector:
112
+ matchLabels:
113
+ toa/composition: mono
114
+ # only this revision's pods: a rollout spreads the new ones, whatever the old ones occupy
115
+ matchLabelKeys:
116
+ - pod-template-hash
117
+ {{- if .backends }}
118
+ ---
119
+ apiVersion: v1
120
+ kind: Service
121
+ metadata:
122
+ name: mono
123
+ {{- if .annotations }}
124
+ annotations:
125
+ {{ toYaml .annotations | indent 4 }}
126
+ {{- end }}
127
+ spec:
128
+ type: ClusterIP
129
+ selector:
130
+ toa/composition: mono
131
+ ports:
132
+ {{- range .backends }}
133
+ - name: port-{{ .port }}
134
+ protocol: TCP
135
+ port: {{ .port }}
136
+ targetPort: {{ .port }}
137
+ {{- end }}
138
+ {{- end }}
139
+ {{- if .ingress }}
140
+ ---
141
+ apiVersion: networking.k8s.io/v1
142
+ kind: Ingress
143
+ metadata:
144
+ name: mono
145
+ {{- if .ingress.annotations }}
146
+ annotations:
147
+ {{ toYaml .ingress.annotations | indent 4 }}
148
+ {{- end }}
149
+ spec:
150
+ {{- if .ingress.class }}
151
+ ingressClassName: {{ .ingress.class }}
152
+ {{- end }}
153
+ {{- $backends := .backends }}
154
+ rules:
155
+ {{- range .ingress.hosts }}
156
+ - host: {{ . }}
157
+ http:
158
+ paths:
159
+ {{- range $backends }}
160
+ - path: {{ .path }}
161
+ pathType: Prefix
162
+ backend:
163
+ service:
164
+ name: mono
165
+ port:
166
+ number: {{ .port }}
167
+ {{- end }}
168
+ {{- end }}
169
+ {{- if .ingress.default }}
170
+ - http:
171
+ paths:
172
+ {{- range $backends }}
173
+ - path: {{ .path }}
174
+ pathType: Prefix
175
+ backend:
176
+ service:
177
+ name: mono
178
+ port:
179
+ number: {{ .port }}
180
+ {{- end }}
181
+ {{- end }}
182
+ {{- end }}
183
+ ---
184
+ {{- end }}
185
+ {{- 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,66 @@ 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
33
75
  {{- end }}
76
+ lifecycle:
77
+ preStop:
78
+ exec:
79
+ command: [sleep, "5"]
80
+ terminationGracePeriodSeconds: 45
81
+ topologySpreadConstraints:
82
+ - maxSkew: 1
83
+ topologyKey: kubernetes.io/hostname
84
+ whenUnsatisfiable: ScheduleAnyway
85
+ labelSelector:
86
+ matchLabels:
87
+ toa/service: extension-{{ .name }}
88
+ # only this revision's pods: a rollout spreads the new ones, whatever the old ones occupy
89
+ matchLabelKeys:
90
+ - pod-template-hash
91
+ {{- end }}
92
+ {{- if .port }}
34
93
  ---
35
94
  apiVersion: v1
36
95
  kind: Service
37
96
  metadata:
38
97
  name: extension-{{ .name }}
98
+ {{- if .annotations }}
99
+ annotations:
100
+ {{ toYaml .annotations | indent 4 }}
101
+ {{- end }}
39
102
  spec:
40
103
  type: ClusterIP
41
104
  selector:
42
- toa.io/service: extension-{{ .name }}
105
+ toa/service-{{ .name }}: "1"
43
106
  ports:
44
107
  - name: port-{{ .port }}
45
108
  protocol: TCP
46
109
  port: {{ .port }}
47
110
  targetPort: {{ .port }}
48
- ---
111
+ {{- end }}
49
112
  {{- if .ingress }}
50
113
  {{- $service := .name }}
114
+ {{- $port := .port }}
115
+ {{- $path := .ingress.path | default "/" }}
116
+ ---
51
117
  apiVersion: networking.k8s.io/v1
52
118
  kind: Ingress
53
119
  metadata:
@@ -65,13 +131,25 @@ spec:
65
131
  - host: {{ . }}
66
132
  http:
67
133
  paths:
68
- - path: /
134
+ - path: {{ $path }}
135
+ pathType: Prefix
136
+ backend:
137
+ service:
138
+ name: extension-{{ $service }}
139
+ port:
140
+ number: {{ $port }}
141
+ {{- end }}
142
+ {{- if .ingress.default }}
143
+ - http:
144
+ paths:
145
+ - path: {{ $path }}
69
146
  pathType: Prefix
70
147
  backend:
71
148
  service:
72
149
  name: extension-{{ $service }}
73
150
  port:
74
- number: 8000
151
+ number: {{ $port }}
75
152
  {{- end }}
76
153
  {{- end }}
154
+ ---
77
155
  {{- 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,18 @@
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
+ // the extensions whose services this composition runs, as `TOA_SERVICES` for the process
14
+ if (composition.services !== undefined) this.services = composition.services
13
15
  }
14
16
  }
15
17
 
16
18
  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
+ }