@toa.io/operations 1.0.0-alpha.253 → 1.0.0-alpha.256

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
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.256](https://github.com/toa-io/toa/compare/v1.0.0-alpha.255...v1.0.0-alpha.256) (2026-08-23)
7
+
8
+
9
+ ### Features
10
+
11
+ * deploy a single mono image with toa deploy --mono ([a7f14f9](https://github.com/toa-io/toa/commit/a7f14f9877a280e9c74d16c195028b35d74fb15a))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/operations",
3
- "version": "1.0.0-alpha.253",
3
+ "version": "1.0.0-alpha.256",
4
4
  "description": "Toa Deployment",
5
5
  "homepage": "https://toa.io",
6
6
  "author": {
@@ -16,7 +16,7 @@
16
16
  "url": "https://github.com/toa-io/toa/issues"
17
17
  },
18
18
  "engines": {
19
- "node": ">= 20.0.0"
19
+ "node": ">= 24.0.0"
20
20
  },
21
21
  "publishConfig": {
22
22
  "access": "public"
@@ -27,12 +27,12 @@
27
27
  "test": "echo \"Error: run tests from root\" && exit 1"
28
28
  },
29
29
  "dependencies": {
30
- "@toa.io/filesystem": "1.0.0-alpha.225",
31
- "@toa.io/generic": "1.0.0-alpha.225",
32
- "@toa.io/norm": "1.0.0-alpha.253",
33
- "@toa.io/yaml": "1.0.0-alpha.246",
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.256",
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": "ca97991a1c8a6e770a51e0899b0c5dbeacbb89ed"
37
+ "gitHead": "a8990df97f971060686bd70a965f78e98070a983"
38
38
  }
@@ -1,8 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const desc = require('./.describe')
4
+ const { addVariables } = require('./.describe/variables')
5
+ const { addMounts } = require('./.describe/mounts')
4
6
 
5
- const describe = (context, compositions, dependency) => {
7
+ const describe = (context, compositions, dependency, image) => {
6
8
  const { services } = dependency
7
9
 
8
10
  dependency.variables.global ??= []
@@ -17,9 +19,24 @@ const describe = (context, compositions, dependency) => {
17
19
  }
18
20
  )
19
21
 
20
- const components = desc.components(compositions)
21
22
  const credentials = context.registry?.credentials
22
23
 
24
+ if (image !== undefined) {
25
+ const mono = unit(context, dependency)
26
+
27
+ mono.image = image.reference
28
+
29
+ return {
30
+ compositions: [],
31
+ components: mono.components,
32
+ services: [],
33
+ credentials,
34
+ mono
35
+ }
36
+ }
37
+
38
+ const components = desc.components(compositions)
39
+
23
40
  desc.compositions(compositions, dependency)
24
41
  desc.services(services, dependency.variables, dependency.probe)
25
42
 
@@ -31,4 +48,39 @@ const describe = (context, compositions, dependency) => {
31
48
  }
32
49
  }
33
50
 
51
+ function unit (context, dependency) {
52
+ const components = (context.components ?? []).map((component) => component.locator.label)
53
+
54
+ const mono = {
55
+ replicas: context.mono?.replicas,
56
+ resources: context.mono?.resources,
57
+ components,
58
+ variables: []
59
+ }
60
+
61
+ addVariables(mono, dependency.variables ?? {})
62
+ addMounts(mono, dependency.mounts)
63
+
64
+ for (const service of dependency.services ?? []) {
65
+ if (service.variables !== undefined)
66
+ for (const variable of service.variables)
67
+ if (!mono.variables.some((item) => item.name === variable.name))
68
+ mono.variables.push(variable)
69
+
70
+ if (service.port !== undefined)
71
+ mono.port = service.port
72
+
73
+ if (service.ingress !== undefined)
74
+ mono.ingress = service.ingress
75
+
76
+ if (service.probe !== undefined && service.probe !== false)
77
+ mono.probe = service.probe
78
+ }
79
+
80
+ if (mono.probe === undefined && dependency.probe !== undefined && dependency.probe !== false)
81
+ mono.probe = dependency.probe
82
+
83
+ return mono
84
+ }
85
+
34
86
  exports.describe = describe
@@ -0,0 +1,168 @@
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: 50%
15
+ selector:
16
+ matchLabels:
17
+ toa.io/composition: mono
18
+ template:
19
+ metadata:
20
+ labels:
21
+ toa.io/composition: mono
22
+ {{- range .components }}
23
+ {{ . }}: "1"
24
+ {{- end }}
25
+ spec:
26
+ containers:
27
+ - name: mono
28
+ image: {{ .image }}
29
+ {{- if .port }}
30
+ ports:
31
+ - containerPort: {{ .port }}
32
+ {{- else if .probe }}
33
+ ports:
34
+ - containerPort: {{ .probe.port }}
35
+ {{- end }}
36
+ {{- if .resources }}
37
+ resources:
38
+ {{- if or .resources.cpu .resources.memory }}
39
+ requests:
40
+ {{- if .resources.cpu }}
41
+ cpu: {{ index .resources.cpu 0 }}
42
+ {{- end }}
43
+ {{- if .resources.memory }}
44
+ memory: {{ index .resources.memory 0 }}
45
+ {{- end }}
46
+ limits:
47
+ {{- if .resources.cpu }}
48
+ cpu: {{ index .resources.cpu 1 }}
49
+ {{- end }}
50
+ {{- if .resources.memory }}
51
+ memory: {{ index .resources.memory 1 }}
52
+ {{- end }}
53
+ {{- end }}
54
+ {{- end }}
55
+ {{- if .variables }}
56
+ env:
57
+ {{- range .variables }}
58
+ {{- include "env.var" . | indent 12 }}
59
+ {{- end }}
60
+ {{- end }}
61
+ {{- if .probe }}
62
+ startupProbe:
63
+ httpGet:
64
+ path: {{ .probe.path }}
65
+ port: {{ .probe.port }}
66
+ {{- if .probe.delay }}
67
+ initialDelaySeconds: {{ .probe.delay }}
68
+ {{- end }}
69
+ periodSeconds: 2
70
+ timeoutSeconds: 3
71
+ failureThreshold: 150
72
+ readinessProbe:
73
+ httpGet:
74
+ path: {{ .probe.path }}
75
+ port: {{ .probe.port }}
76
+ periodSeconds: 10
77
+ timeoutSeconds: 3
78
+ failureThreshold: 3
79
+ {{- end }}
80
+ {{- if .mounts }}
81
+ volumeMounts:
82
+ {{- range .mounts }}
83
+ - name: {{ .name }}
84
+ mountPath: {{ .path }}
85
+ {{- end }}
86
+ {{- end }}
87
+ lifecycle:
88
+ preStop:
89
+ exec:
90
+ command: [sleep, "5"]
91
+ terminationGracePeriodSeconds: 45
92
+ {{- if $.Values.credentials }}
93
+ imagePullSecrets:
94
+ - name: {{ $.Values.credentials }}
95
+ {{- end }}
96
+ {{- if .mounts }}
97
+ volumes:
98
+ {{- range .mounts }}
99
+ - name: {{ .name }}
100
+ persistentVolumeClaim:
101
+ claimName: {{ .claim }}
102
+ {{- end }}
103
+ {{- end }}
104
+ topologySpreadConstraints:
105
+ - maxSkew: 1
106
+ topologyKey: kubernetes.io/hostname
107
+ whenUnsatisfiable: ScheduleAnyway
108
+ labelSelector:
109
+ matchLabels:
110
+ toa.io/composition: mono
111
+ {{- if .port }}
112
+ ---
113
+ apiVersion: v1
114
+ kind: Service
115
+ metadata:
116
+ name: mono
117
+ spec:
118
+ type: ClusterIP
119
+ selector:
120
+ toa.io/composition: mono
121
+ ports:
122
+ - name: port-{{ .port }}
123
+ protocol: TCP
124
+ port: {{ .port }}
125
+ targetPort: {{ .port }}
126
+ {{- end }}
127
+ {{- if .ingress }}
128
+ ---
129
+ apiVersion: networking.k8s.io/v1
130
+ kind: Ingress
131
+ metadata:
132
+ name: mono
133
+ {{- if .ingress.annotations }}
134
+ annotations:
135
+ {{ toYaml .ingress.annotations | indent 4 }}
136
+ {{- end }}
137
+ spec:
138
+ {{- if .ingress.class }}
139
+ ingressClassName: {{ .ingress.class }}
140
+ {{- end }}
141
+ rules:
142
+ {{- range .ingress.hosts }}
143
+ - host: {{ . }}
144
+ http:
145
+ paths:
146
+ - path: /
147
+ pathType: Prefix
148
+ backend:
149
+ service:
150
+ name: mono
151
+ port:
152
+ number: {{ $.Values.mono.port | default 8000 }}
153
+ {{- end }}
154
+ {{- if .ingress.default }}
155
+ - http:
156
+ paths:
157
+ - path: /
158
+ pathType: Prefix
159
+ backend:
160
+ service:
161
+ name: mono
162
+ port:
163
+ number: {{ $.Values.mono.port | default 8000 }}
164
+ {{- end }}
165
+ {{- end }}
166
+ ---
167
+ {{- end }}
168
+ {{- end }}
@@ -13,11 +13,11 @@ class Deployment {
13
13
  #process
14
14
  #target
15
15
 
16
- constructor (context, compositions, dependencies, process) {
16
+ constructor (context, compositions, dependencies, process, image) {
17
17
  const dependency = merge(dependencies)
18
18
 
19
19
  this.#chart = declare(context, dependency)
20
- this.#values = describe(context, compositions, dependency)
20
+ this.#values = describe(context, compositions, dependency, image)
21
21
  this.#process = process
22
22
  }
23
23
 
@@ -69,6 +69,9 @@ class Deployment {
69
69
  addVariables(this.#values.compositions, variables, used)
70
70
  addVariables(this.#values.services, variables, used)
71
71
 
72
+ if (this.#values.mono !== undefined)
73
+ addVariables([this.#values.mono], variables, used)
74
+
72
75
  return variables
73
76
  }
74
77
  }
@@ -11,24 +11,42 @@ const { Service } = require('./service')
11
11
 
12
12
  class Factory {
13
13
  #context
14
+ #root
15
+ #mono
14
16
  #compositions
15
17
  #dependencies
16
18
  #registry
17
19
  #process
20
+ #image
18
21
 
19
- constructor (context) {
22
+ constructor (context, options = {}) {
20
23
  this.#context = context
24
+ this.#root = options.root
25
+ this.#mono = options.mono === true
21
26
  this.#process = new Process()
22
27
 
23
28
  const imagesFactory = new ImagesFactory(context.name, context.runtime, context.registry)
24
29
 
25
30
  this.#registry = new Registry(context.name, context.registry, imagesFactory, this.#process)
26
- this.#compositions = context.compositions.map((composition) => this.#composition(composition))
27
31
  this.#dependencies = this.#getDependencies()
32
+ this.#compositions = []
33
+
34
+ if (this.#mono)
35
+ this.#image = this.#registry.mono({
36
+ components: context.components
37
+ }, this.#root)
38
+ else
39
+ this.#compositions = context.compositions.map((composition) => this.#composition(composition))
28
40
  }
29
41
 
30
42
  operator () {
31
- const deployment = new Deployment(this.#context, this.#compositions, this.#dependencies, this.#process)
43
+ const deployment = new Deployment(
44
+ this.#context,
45
+ this.#compositions,
46
+ this.#dependencies,
47
+ this.#process,
48
+ this.#image
49
+ )
32
50
 
33
51
  return new Operator(deployment, this.#registry)
34
52
  }
@@ -70,7 +88,8 @@ class Factory {
70
88
  const dependency = module.deployment(instances, annotation)
71
89
 
72
90
  /** @type {toa.deployment.Service[]} */
73
- const services = dependency.services?.map((service) => this.#service(path, service))
91
+ const services = dependency.services?.map((service) =>
92
+ this.#mono ? service : this.#service(path, service))
74
93
 
75
94
  return { ...dependency, services }
76
95
  }
@@ -86,10 +105,10 @@ class Factory {
86
105
  return new Service(service, image)
87
106
  }
88
107
 
89
- static async create (path, environment) {
108
+ static async create (path, environment, options = {}) {
90
109
  const context = await load(path, environment)
91
110
 
92
- return new Factory(context)
111
+ return new Factory(context, { ...options, root: path })
93
112
  }
94
113
  }
95
114
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { Composition } = require('./composition')
4
4
  const { Service } = require('./service')
5
+ const { Mono } = require('./mono')
5
6
 
6
7
  class Factory {
7
8
  /** @type {string} */
@@ -45,6 +46,17 @@ class Factory {
45
46
 
46
47
  return instance
47
48
  }
49
+
50
+ /**
51
+ * @returns {Mono}
52
+ */
53
+ mono (composition, root) {
54
+ const instance = new Mono(this.#scope, this.#runtime, this.#registry, composition, root)
55
+
56
+ instance.tag()
57
+
58
+ return instance
59
+ }
48
60
  }
49
61
 
50
62
  exports.Factory = Factory
@@ -0,0 +1,18 @@
1
+ FROM {{build.image}}
2
+
3
+ {{build.arguments}}
4
+
5
+ ENV NODE_ENV=production
6
+ RUN if [ "{{runtime.registry}}" != "" ]; then npm set registry {{runtime.registry}}; fi
7
+ RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
8
+
9
+ WORKDIR /composition
10
+ COPY --chown=node:node . /composition
11
+
12
+ {{build.run}}
13
+
14
+ RUN --mount=type=cache,target=/root/.npm \
15
+ for entry in components/*; do if [ -f "$entry/package.json" ]; then (cd $entry && npm i --omit=dev); fi; done
16
+
17
+ USER node
18
+ CMD toa mono .
@@ -0,0 +1,82 @@
1
+ 'use strict'
2
+
3
+ const { join } = require('node:path')
4
+ const fs = require('fs-extra')
5
+ const { createHash } = require('node:crypto')
6
+
7
+ const { Image } = require('./image')
8
+
9
+ class Mono extends Image {
10
+ dockerfile = join(__dirname, 'mono.Dockerfile')
11
+
12
+ #root
13
+ #image
14
+ #components
15
+
16
+ constructor (scope, runtime, registry, composition, root) {
17
+ super(scope, runtime, registry)
18
+
19
+ this.#root = root
20
+ this.#image = composition.image
21
+ this.#components = composition.components
22
+ }
23
+
24
+ get name () {
25
+ return 'mono'
26
+ }
27
+
28
+ get version () {
29
+ const hash = createHash('sha256')
30
+
31
+ for (const component of this.#components) {
32
+ hash.update(component.locator.id)
33
+ hash.update(component.version)
34
+ }
35
+
36
+ return hash.digest('hex').slice(0, 8)
37
+ }
38
+
39
+ get base () {
40
+ let image = this.#image
41
+
42
+ for (const component of this.#components) {
43
+ const value = component.build?.image
44
+
45
+ if (image !== null && image !== value)
46
+ throw new Error('Mono deployment requires different base images for its components. Specify base image for the composition in the context.')
47
+
48
+ image = value
49
+ }
50
+
51
+ return image
52
+ }
53
+
54
+ get run () {
55
+ const commands = []
56
+
57
+ for (const component of this.#components) {
58
+ const run = component.build?.run
59
+
60
+ if (run !== undefined)
61
+ commands.push(run)
62
+ }
63
+
64
+ return commands.join('\n')
65
+ }
66
+
67
+ async prepare (root) {
68
+ const context = await super.prepare(root)
69
+
70
+ await fs.copy(join(this.#root, CONTEXT), join(context, CONTEXT))
71
+ await fs.ensureDir(join(context, 'components'))
72
+
73
+ for (const component of this.#components)
74
+ await fs.copy(component.path, join(context, 'components', component.locator.label))
75
+
76
+ return context
77
+ }
78
+ }
79
+
80
+ const CONTEXT = 'context.toa.yaml'
81
+
82
+ exports.Mono = Mono
@@ -35,6 +35,10 @@ class Registry {
35
35
  return this.#create('service', path, service)
36
36
  }
37
37
 
38
+ mono (composition, root) {
39
+ return this.#create('mono', composition, root)
40
+ }
41
+
38
42
  async prepare (root) {
39
43
  const path = await workspace.create('images', root)
40
44
 
@@ -61,7 +65,7 @@ class Registry {
61
65
  }
62
66
 
63
67
  /**
64
- * @param {'composition' | 'service'} type
68
+ * @param {'composition' | 'service' | 'mono'} type
65
69
  * @param {...any} args
66
70
  * @returns {toa.deployment.images.Image}
67
71
  */