@toa.io/operations 1.0.0-alpha.33 → 1.0.0-alpha.330

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 (78) hide show
  1. package/CHANGELOG.md +371 -0
  2. package/image/runtime.Dockerfile +20 -0
  3. package/package.json +10 -10
  4. package/readme.md +224 -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 +160 -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 +13 -0
  24. package/src/deployment/chart/templates/components.yaml +4 -3
  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 +172 -34
  31. package/src/deployment/drain.js +70 -0
  32. package/src/deployment/drain.test.js +64 -0
  33. package/src/deployment/factory.js +172 -37
  34. package/src/deployment/images/bundle.js +93 -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 +271 -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 +150 -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 +25 -16
  55. package/src/deployment/operator.test.js +98 -7
  56. package/src/deployment/registry.js +210 -46
  57. package/src/deployment/registry.test.js +539 -0
  58. package/src/deployment/secrets.js +92 -0
  59. package/src/deployment/secrets.test.js +180 -0
  60. package/src/deployment/service.js +4 -6
  61. package/src/deployment/workspace.js +13 -8
  62. package/src/index.js +3 -2
  63. package/src/process.js +51 -13
  64. package/src/process.test.js +33 -0
  65. package/types/_deployment/composition.d.ts +2 -3
  66. package/types/_deployment/dependency.d.ts +15 -7
  67. package/types/_deployment/deployment.d.ts +10 -8
  68. package/types/_deployment/factory.d.ts +2 -4
  69. package/types/_deployment/images/factory.d.ts +6 -8
  70. package/types/_deployment/images/image.d.ts +16 -1
  71. package/types/_deployment/images/registry.d.ts +8 -11
  72. package/types/_deployment/operator.d.ts +10 -9
  73. package/types/_deployment/registry.d.ts +7 -4
  74. package/types/_deployment/service.d.ts +4 -3
  75. package/types/dependency.ts +79 -0
  76. package/types/index.ts +1 -0
  77. package/types/dependency.d.ts +0 -39
  78. 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,228 @@
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'
9
+ import * as secrets from './secrets.js'
7
10
 
8
- const { merge, declare, describe } = require('./.deployment')
9
-
10
- class Deployment {
11
+ export class Deployment {
11
12
  #chart
12
13
  #values
14
+ #keyed
13
15
  #process
14
16
  #target
15
17
 
16
- constructor (context, compositions, dependencies, process) {
18
+ constructor(context, compositions, dependencies, process, image) {
17
19
  const dependency = merge(dependencies)
18
20
 
19
21
  this.#chart = declare(context, dependency)
20
- this.#values = describe(context, compositions, dependency)
22
+ this.#values = describe(context, compositions, dependency, image)
23
+ this.#keyed = dependency.variables
21
24
  this.#process = process
22
25
  }
23
26
 
24
- async export (target) {
27
+ async export(target) {
28
+ // a context is read for a local run as well — `toa env` builds one of these and asks it
29
+ // for variables — and such a context needs no chart and states no version. So this is
30
+ // asked for where a chart is written, and not where a context is read. Without it Helm
31
+ // answers `chart.metadata.version is required`, naming nothing, by which point a deploy
32
+ // has built and pushed every image.
33
+ if (this.#chart.version === undefined)
34
+ throw new Error(
35
+ `Context '${this.#chart.name}' declares no version, which is what its chart is ` +
36
+ 'versioned by. Declare it in context.toa.yaml.'
37
+ )
38
+
25
39
  const chart = dump(this.#chart)
26
40
  const values = dump(this.#values)
27
41
 
28
42
  await Promise.all([
29
43
  write(join(target, 'Chart.yaml'), chart),
30
44
  write(join(target, 'values.yaml'), values),
31
- fs.copy(TEMPLATES, join(target, 'templates'))
45
+ cp(TEMPLATES, join(target, 'templates'), { recursive: true })
32
46
  ])
33
47
 
34
48
  this.#target = target
35
49
  }
36
50
 
37
- async install (options) {
51
+ /**
52
+ * Refuses a deploy the cluster is not ready for: a secret a workload reads that is not there
53
+ * leaves its pod uncreated, which is seen only once every image has been built and pushed.
54
+ *
55
+ * @param {toa.deployment.installation.Options} options
56
+ * @returns {Promise<void>}
57
+ */
58
+ async verify(options) {
59
+ const references = secrets.references(this.variables(), this.#values.credentials)
60
+
61
+ await secrets.verify(this.#process, references, options)
62
+ }
63
+
64
+ async install(options) {
38
65
  if (options.target) this.#target = options.target
39
- if (this.#target === undefined) throw new Error('Deployment hasn\'t been exported')
66
+ if (this.#target === undefined) throw new Error("Deployment hasn't been exported")
40
67
 
41
68
  const args = []
42
69
 
43
70
  if (options.namespace !== undefined) args.push('-n', options.namespace)
44
71
  if (options.wait === true) args.push('--wait')
72
+ if (options.timeout !== undefined) args.push('--timeout', options.timeout)
73
+
74
+ await this.#update()
75
+ await this.#process.execute('helm', [
76
+ 'upgrade',
77
+ this.#chart.name,
78
+ '-i',
79
+ ...args,
80
+ this.#target
81
+ ])
45
82
 
46
- await this.#process.execute('helm', ['dependency', 'update', this.#target])
47
- await this.#process.execute('helm', ['upgrade', this.#chart.name, '-i', ...args, this.#target])
83
+ // ready is not done: the replicas replaced are still draining when helm answers
84
+ if (options.wait === true) await drain(this.#process, options)
48
85
  }
49
86
 
50
- async template (options) {
51
- if (this.#target === undefined) throw new Error('Deployment hasn\'t been exported')
87
+ async template(options) {
88
+ if (this.#target === undefined) throw new Error("Deployment hasn't been exported")
52
89
 
53
- await this.#process.execute('helm', ['dependency', 'update', this.#target], { silently: true })
90
+ await this.#update({ silently: true })
54
91
 
55
92
  const args = []
56
93
 
57
94
  if (options.namespace !== undefined) args.push('-n', options.namespace)
58
95
 
59
- return await this.#process.execute('helm',
96
+ return await this.#process.execute(
97
+ 'helm',
60
98
  ['template', this.#chart.name, ...args, this.#target],
61
- { silently: true })
99
+ { silently: true }
100
+ )
101
+ }
102
+
103
+ /**
104
+ * A chart with no subcharts has nothing to resolve, and the call still reaches out for
105
+ * the repositories it would have read.
106
+ *
107
+ * @param {object} [options]
108
+ * @returns {Promise<void>}
109
+ */
110
+ async #update(options = {}) {
111
+ if (this.#chart.dependencies.length === 0) return
112
+
113
+ await this.#process.execute('helm', ['dependency', 'update', this.#target], options)
114
+ }
115
+
116
+ /**
117
+ * @param {{ components?: string[], services?: string[] }} [options]
118
+ * @returns {toa.deployment.dependency.Variable[]}
119
+ */
120
+ variables(options = {}) {
121
+ if (options.components === undefined && options.services === undefined) {
122
+ const variables = []
123
+ const used = new Set()
124
+
125
+ addVariables(this.#values.compositions, variables, used)
126
+ addVariables(this.#values.services, variables, used)
127
+
128
+ if (this.#values.mono !== undefined)
129
+ addVariables([this.#values.mono], variables, used)
130
+
131
+ return variables
132
+ }
133
+
134
+ return this.#select(options.components, options.services)
62
135
  }
63
136
 
64
- variables () {
137
+ /**
138
+ * @param {string[] | undefined} components
139
+ * @param {string[] | undefined} services
140
+ * @returns {toa.deployment.dependency.Variable[]}
141
+ */
142
+ #select(components, services) {
143
+ const labels = labelsOf(this.#values, this.#keyed)
144
+ const keys = new Set(['global'])
145
+ const selected = []
146
+
147
+ for (const token of components ?? []) keys.add(matchComponent(token, labels))
148
+
149
+ for (const token of services ?? []) {
150
+ const service = matchService(token, this.#values.services)
151
+
152
+ selected.push(service)
153
+
154
+ for (const label of service.components ?? []) keys.add(label)
155
+ }
156
+
65
157
  const variables = []
66
158
  const used = new Set()
67
159
 
68
- addVariables(this.#values.compositions, variables, used)
69
- addVariables(this.#values.services, variables, used)
160
+ for (const key of keys) append(this.#keyed[key], variables, used)
161
+
162
+ for (const service of selected) append(service.variables, variables, used)
70
163
 
71
164
  return variables
72
165
  }
73
166
  }
74
167
 
75
- function addVariables (list, variables, used = new Set()) {
76
- for (const item of list) {
77
- if (item.variables === undefined) continue
168
+ function addVariables(list, variables, used = new Set()) {
169
+ if (list === undefined) return
78
170
 
79
- for (const variable of item.variables) {
80
- if (used.has(variable.name)) continue
171
+ for (const item of list) append(item.variables, variables, used)
172
+ }
81
173
 
82
- variables.push(variable)
83
- used.add(variable.name)
84
- }
174
+ function append(set, variables, used) {
175
+ if (set === undefined) return
176
+
177
+ for (const variable of set) {
178
+ if (used.has(variable.name)) continue
179
+
180
+ variables.push(variable)
181
+ used.add(variable.name)
85
182
  }
86
183
  }
87
184
 
88
- const TEMPLATES = join(__dirname, 'chart/templates')
185
+ function labelsOf(values, keyed) {
186
+ const labels = new Set(values.components ?? [])
89
187
 
90
- exports.Deployment = Deployment
188
+ for (const service of values.services ?? [])
189
+ for (const label of service.components ?? []) labels.add(label)
190
+
191
+ for (const key of Object.keys(keyed ?? {})) if (key !== 'global') labels.add(key)
192
+
193
+ return labels
194
+ }
195
+
196
+ function matchComponent(token, labels) {
197
+ const candidates = [token, token.replaceAll('.', '-').toLowerCase()]
198
+
199
+ if (!token.includes('.')) candidates.push('default-' + token.toLowerCase())
200
+
201
+ for (const candidate of candidates) if (labels.has(candidate)) return candidate
202
+
203
+ throw new Error(`Component '${token}' is not in the context`)
204
+ }
205
+
206
+ function matchService(token, services) {
207
+ const resolved = shortcuts.resolve(token)
208
+
209
+ const service = (services ?? []).find((service) => {
210
+ if (service.name === token || service.group === token) return true
211
+
212
+ for (const [alias, pkg] of Object.entries(shortcuts.SHORTCUTS))
213
+ if (resolved === pkg && service.group === alias) return true
214
+
215
+ return false
216
+ })
217
+
218
+ if (service === undefined) throw new Error(`Service '${token}' is not in the context`)
219
+
220
+ return service
221
+ }
222
+
223
+ const TEMPLATES = join(import.meta.dirname, 'chart/templates')
224
+
225
+ function dump(object) {
226
+ // js-yaml writes plain objects only, and the values carry locators and images
227
+ return jsyaml.dump(JSON.parse(JSON.stringify(object)), { noRefs: true, lineWidth: -1 })
228
+ }