@toa.io/operations 1.0.0-alpha.28 → 1.0.0-alpha.280

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 (64) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/image/runtime.Dockerfile +4 -0
  3. package/package.json +10 -10
  4. package/readme.md +16 -0
  5. package/src/deployment/.deployment/.describe/components.js +1 -5
  6. package/src/deployment/.deployment/.describe/compositions.js +8 -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/index.js +4 -13
  10. package/src/deployment/.deployment/.describe/mounts.js +20 -0
  11. package/src/deployment/.deployment/.describe/resources.js +33 -0
  12. package/src/deployment/.deployment/.describe/services.js +28 -5
  13. package/src/deployment/.deployment/.describe/services.test.js +61 -0
  14. package/src/deployment/.deployment/.describe/variables.js +6 -9
  15. package/src/deployment/.deployment/declare.js +1 -5
  16. package/src/deployment/.deployment/describe.js +133 -9
  17. package/src/deployment/.deployment/index.js +3 -9
  18. package/src/deployment/.deployment/merge.js +46 -6
  19. package/src/deployment/.deployment/merge.test.js +72 -0
  20. package/src/deployment/chart/templates/compositions.yaml +70 -0
  21. package/src/deployment/chart/templates/mono.yaml +181 -0
  22. package/src/deployment/chart/templates/services.yaml +75 -4
  23. package/src/deployment/chart/values.yaml +11 -1
  24. package/src/deployment/composition.js +2 -5
  25. package/src/deployment/deployment.js +19 -12
  26. package/src/deployment/factory.js +50 -28
  27. package/src/deployment/images/composition.Dockerfile +2 -2
  28. package/src/deployment/images/composition.js +26 -22
  29. package/src/deployment/images/factory.js +15 -10
  30. package/src/deployment/images/format.js +48 -0
  31. package/src/deployment/images/format.test.js +74 -0
  32. package/src/deployment/images/image.fixtures.js +11 -11
  33. package/src/deployment/images/image.js +20 -22
  34. package/src/deployment/images/image.test.js +6 -5
  35. package/src/deployment/images/index.js +1 -5
  36. package/src/deployment/images/mono.Dockerfile +18 -0
  37. package/src/deployment/images/mono.js +72 -0
  38. package/src/deployment/images/runtime-base.test.js +91 -0
  39. package/src/deployment/images/service.Dockerfile +3 -3
  40. package/src/deployment/images/service.js +8 -8
  41. package/src/deployment/index.js +1 -5
  42. package/src/deployment/operator.js +6 -6
  43. package/src/deployment/operator.test.js +8 -7
  44. package/src/deployment/registry.js +66 -22
  45. package/src/deployment/registry.test.js +177 -0
  46. package/src/deployment/service.js +1 -5
  47. package/src/deployment/workspace.js +13 -8
  48. package/src/index.js +1 -3
  49. package/src/process.js +2 -8
  50. package/src/process.test.js +20 -0
  51. package/types/_deployment/composition.d.ts +1 -1
  52. package/types/_deployment/dependency.d.ts +3 -1
  53. package/types/_deployment/deployment.d.ts +3 -3
  54. package/types/_deployment/factory.d.ts +2 -2
  55. package/types/_deployment/images/factory.d.ts +2 -2
  56. package/types/_deployment/images/image.d.ts +4 -2
  57. package/types/_deployment/images/registry.d.ts +2 -2
  58. package/types/_deployment/operator.d.ts +2 -2
  59. package/types/_deployment/registry.d.ts +10 -8
  60. package/types/_deployment/service.d.ts +4 -1
  61. package/types/dependency.ts +73 -0
  62. package/types/index.ts +1 -0
  63. package/types/dependency.d.ts +0 -39
  64. package/types/index.d.ts +0 -1
@@ -0,0 +1,177 @@
1
+ import { it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
4
+
5
+ import { Registry } from './registry.js'
6
+
7
+ /** @type {toa.operations.Process} */
8
+ let process
9
+
10
+ /** @type {toa.deployment.images.Factory} */
11
+ let factory
12
+
13
+ /** @type {toa.deployment.images.Image[]} */
14
+ let images
15
+
16
+ beforeEach(() => {
17
+ images = []
18
+ process = /** @type {toa.operations.Process} */ {
19
+ execute: mock.fn(async (cmd, args) => {
20
+ if (args[0] === 'manifest')
21
+ throw new Error('manifest unknown')
22
+
23
+ if (args[0] === 'buildx' && args[1] === 'inspect')
24
+ throw new Error('builder not found')
25
+
26
+ return ''
27
+ })
28
+ }
29
+
30
+ factory = /** @type {toa.deployment.images.Factory} */ {
31
+ composition: () => createImage('composition-mono'),
32
+ service: () => createImage('extension-realtime')
33
+ }
34
+ })
35
+
36
+ it('should reuse named builder across images', async () => {
37
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
38
+
39
+ registry.composition(/** @type {any} */ ({}))
40
+ registry.service('.', /** @type {any} */ ({}))
41
+
42
+ await registry.build()
43
+
44
+ const creates = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
45
+ args[0] === 'buildx' && args[1] === 'create')
46
+
47
+ assert.strictEqual(creates.length, 1)
48
+ assert.deepStrictEqual(creates[0].arguments[1], ['buildx', 'create', '--name', 'toa', '--bootstrap'])
49
+
50
+ const builds = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
51
+ args[0] === '--context=default' && args[1] === 'buildx' && args[2] === 'build')
52
+
53
+ assert.strictEqual(builds.length, 2)
54
+
55
+ for (const { arguments: [, args] } of builds) {
56
+ assert.ok(args.includes('--builder'))
57
+ assert.strictEqual(args[args.indexOf('--builder') + 1], 'toa')
58
+ }
59
+ })
60
+
61
+ it('should not create builder when it already exists', async () => {
62
+ process.execute = mock.fn(async (cmd, args) => {
63
+ if (args[0] === 'manifest')
64
+ throw new Error('manifest unknown')
65
+
66
+ return ''
67
+ })
68
+
69
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
70
+
71
+ registry.composition(/** @type {any} */ ({}))
72
+
73
+ await registry.build()
74
+
75
+ const creates = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
76
+ args[0] === 'buildx' && args[1] === 'create')
77
+
78
+ assert.strictEqual(creates.length, 0)
79
+ })
80
+
81
+ it('should add shared registry cache flags when base is set', async () => {
82
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
83
+
84
+ registry.composition(/** @type {any} */ ({}))
85
+ registry.service('.', /** @type {any} */ ({}))
86
+
87
+ await registry.build()
88
+
89
+ const builds = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
90
+ args[0] === '--context=default' && args[2] === 'build')
91
+
92
+ assert.strictEqual(builds.length, 2)
93
+
94
+ for (const { arguments: [, args] } of builds) {
95
+ assert.ok(args.includes('--cache-from'))
96
+ assert.ok(args.includes('type=registry,ref=example.com/reg/acme/buildcache'))
97
+ assert.ok(args.includes('--cache-to'))
98
+ assert.ok(args.includes('type=registry,ref=example.com/reg/acme/buildcache,mode=max,image-manifest=true'))
99
+ }
100
+ })
101
+
102
+ it('should omit cache flags when base is not set', async () => {
103
+ const registry = createRegistry({ platforms: ['linux/amd64'] })
104
+
105
+ registry.composition(/** @type {any} */ ({}))
106
+
107
+ await registry.build()
108
+
109
+ const builds = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
110
+ args[0] === '--context=default' && args[2] === 'build')
111
+
112
+ assert.strictEqual(builds.length, 1)
113
+
114
+ const { arguments: [, args] } = builds[0]
115
+
116
+ assert.ok(!(args.includes('--cache-from')))
117
+ assert.ok(!(args.includes('--cache-to')))
118
+ })
119
+
120
+ it('should use default builder when platforms is null', async () => {
121
+ const registry = createRegistry({ base: 'example.com/reg', platforms: null })
122
+
123
+ registry.composition(/** @type {any} */ ({}))
124
+
125
+ await registry.build()
126
+
127
+ const builds = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
128
+ args[0] === '--context=default' && args[2] === 'build')
129
+
130
+ assert.strictEqual(builds.length, 1)
131
+
132
+ const { arguments: [, args] } = builds[0]
133
+
134
+ assert.strictEqual(args[args.indexOf('--builder') + 1], 'default')
135
+ assert.ok(!(args.includes('--cache-from')))
136
+ assert.ok(!(args.includes('--platform')))
137
+ })
138
+
139
+ it('should skip build when image already exists', async () => {
140
+ process.execute = mock.fn(async () => '')
141
+
142
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
143
+
144
+ registry.composition(/** @type {any} */ ({}))
145
+
146
+ await registry.build()
147
+
148
+ const builds = process.execute.mock.calls.filter(({ arguments: [, args] }) =>
149
+ args[0] === '--context=default' && args[2] === 'build')
150
+
151
+ assert.strictEqual(builds.length, 0)
152
+ assert.ok(process.execute.mock.calls.some((call) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], 'docker') && isDeepStrictEqual(call.arguments[1], ['manifest', 'inspect', images[0].reference]) && isDeepStrictEqual(call.arguments[2], { silently: true })))
153
+ })
154
+
155
+ /**
156
+ * @param {object} [registry]
157
+ * @returns {Registry}
158
+ */
159
+ function createRegistry (registry = {}) {
160
+ return new Registry('acme', registry, factory, process)
161
+ }
162
+
163
+ /**
164
+ * @param {string} name
165
+ * @returns {toa.deployment.images.Image}
166
+ */
167
+ function createImage (name) {
168
+ const image = /** @type {toa.deployment.images.Image} */ {
169
+ reference: `example.com/reg/acme/${name}:abcdef12`,
170
+ context: `/tmp/${name}`,
171
+ prepare: mock.fn(async () => `/tmp/${name}`)
172
+ }
173
+
174
+ images.push(image)
175
+
176
+ return image
177
+ }
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- class Service {
1
+ export class Service {
4
2
  constructor (service, image) {
5
3
  Object.assign(this, service)
6
4
 
@@ -8,5 +6,3 @@ class Service {
8
6
  this.image = image.reference
9
7
  }
10
8
  }
11
-
12
- exports.Service = Service
@@ -1,17 +1,22 @@
1
- 'use strict'
2
-
3
- const { directory } = require('@toa.io/filesystem')
1
+ import { mkdir, mkdtemp, readdir } from 'node:fs/promises'
2
+ import { join, resolve } from 'node:path'
3
+ import { tmpdir } from 'node:os'
4
4
 
5
5
  /**
6
6
  * @param {string} type
7
7
  * @param {string} [path]
8
8
  * @return {Promise<string>}
9
9
  */
10
- async function create (type, path) {
11
- if (path === undefined) path = await directory.temp('toa-' + type)
12
- else path = await directory.ensure(path)
10
+ export async function create (type, path) {
11
+ if (path === undefined) return await mkdtemp(join(tmpdir(), 'toa-' + type))
12
+
13
+ path = resolve(path)
14
+
15
+ await mkdir(path, { recursive: true })
16
+
17
+ const entries = await readdir(path)
18
+
19
+ if (entries.length > 0) throw new Error('Target directory must be empty')
13
20
 
14
21
  return path
15
22
  }
16
-
17
- exports.create = create
package/src/index.js CHANGED
@@ -1,3 +1 @@
1
- 'use strict'
2
-
3
- exports.deployment = require('./deployment')
1
+ export * as deployment from './deployment/index.js'
package/src/process.js CHANGED
@@ -1,15 +1,12 @@
1
- 'use strict'
2
-
3
- const execa = require('execa')
1
+ import { execa } from 'execa'
4
2
 
5
3
  /**
6
4
  * @implements {toa.operations.Process}
7
5
  */
8
- class Process {
6
+ export class Process {
9
7
  async execute (cmd, args, options = {}) {
10
8
  console.log('toa>', cmd, args.join(' '))
11
9
 
12
- /** @type {execa.ExecaReturnValue<import('stream').Stream>} */
13
10
  const command = execa(cmd, args)
14
11
 
15
12
  if (options.silently !== true) {
@@ -17,11 +14,8 @@ class Process {
17
14
  command.stderr.pipe(process.stderr)
18
15
  }
19
16
 
20
- /** @type {execa.ExecaReturnValue<string>} */
21
17
  const result = await command
22
18
 
23
19
  return result.stdout
24
20
  }
25
21
  }
26
-
27
- exports.Process = Process
@@ -0,0 +1,20 @@
1
+ import { it, describe } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { Process } from './process.js'
5
+
6
+ describe('execute', () => {
7
+ it('should answer what the command wrote', async () => {
8
+ // `execa` is a named export, and calling the module namespace instead answers
9
+ // `execa is not a function` — where only a deployment would have found out
10
+ const output = await new Process().execute('node', ['-e', 'process.stdout.write("ok")'],
11
+ { silently: true })
12
+
13
+ assert.strictEqual(output, 'ok')
14
+ })
15
+
16
+ it('should reject what the command failed at', async () => {
17
+ await assert.rejects(new Process().execute('node', ['-e', 'process.exit(1)'],
18
+ { silently: true }))
19
+ })
20
+ })
@@ -1,4 +1,4 @@
1
- import type * as _deployment from './deployment'
1
+ import type * as _deployment from './deployment.js'
2
2
 
3
3
  declare namespace toa.deployment {
4
4
 
@@ -1,4 +1,4 @@
1
- import * as _service from './service'
1
+ import * as _service from './service.js'
2
2
  import { dependencies } from '@toa.io/norm/types/context'
3
3
 
4
4
  declare namespace toa.deployment {
@@ -50,6 +50,7 @@ declare namespace toa.deployment {
50
50
  services?: Service[] // dependency.Service
51
51
  proxies?: Proxy[]
52
52
  variables?: Variables
53
+ events?: string[]
53
54
  }
54
55
 
55
56
  }
@@ -59,6 +60,7 @@ declare namespace toa.deployment {
59
60
  services?: _service.Service[] // deployment.Service
60
61
  proxies?: dependency.Proxy[]
61
62
  variables?: dependency.Variables
63
+ events?: string[]
62
64
  }
63
65
 
64
66
  }
@@ -1,6 +1,6 @@
1
- import type * as _composition from './composition'
2
- import type * as _service from './service'
3
- import type * as _dependency from './dependency'
1
+ import type * as _composition from './composition.js'
2
+ import type * as _service from './service.js'
3
+ import type * as _dependency from './dependency.js'
4
4
 
5
5
  declare namespace toa.deployment {
6
6
 
@@ -1,5 +1,5 @@
1
- import type * as _operator from './operator'
2
- import * as _registry from './registry'
1
+ import type * as _operator from './operator.js'
2
+ import * as _registry from './registry.js'
3
3
 
4
4
  declare namespace toa.deployment {
5
5
 
@@ -1,8 +1,8 @@
1
1
  // noinspection ES6UnusedImports
2
2
 
3
3
  import type { Composition } from '@toa.io/norm/types'
4
- import type { Image } from './image'
5
- import type { dependency } from '../dependency'
4
+ import type { Image } from './image.js'
5
+ import type { dependency } from '../dependency.js'
6
6
 
7
7
  declare namespace toa.deployment.images {
8
8
 
@@ -4,9 +4,11 @@ declare namespace toa.deployment.images {
4
4
  readonly reference: string
5
5
  readonly context: string
6
6
 
7
- tag(): void
7
+ name: string
8
8
 
9
- prepare(root: string): Promise<string>
9
+ tag (): void
10
+
11
+ prepare (root: string): Promise<string>
10
12
  }
11
13
 
12
14
  }
@@ -1,8 +1,8 @@
1
1
  // noinspection ES6UnusedImports
2
2
 
3
3
  import type { Composition } from '@toa.io/norm'
4
- import type { dependency } from '../dependency'
5
- import type { Image } from "./image"
4
+ import type { dependency } from '../dependency.js'
5
+ import type { Image } from "./image.js"
6
6
 
7
7
  declare namespace toa.deployment.images {
8
8
 
@@ -1,5 +1,5 @@
1
- import * as _deployment from './deployment'
2
- import * as _dependency from './dependency'
1
+ import * as _deployment from './deployment.js'
2
+ import * as _dependency from './dependency.js'
3
3
 
4
4
  declare namespace toa.deployment {
5
5
 
@@ -1,19 +1,21 @@
1
1
  import type * as _norm from '@toa.io/norm/types'
2
- import type * as _dependency from './dependency'
3
- import type * as _image from "./images/image"
2
+ import type * as _dependency from './dependency.js'
3
+ import type * as _image from './images/image.js'
4
4
 
5
5
  declare namespace toa.deployment {
6
-
6
+
7
7
  interface Registry {
8
- composition(composition: _norm.Composition): _image.Image
8
+ composition (composition: _norm.Composition): _image.Image
9
+
10
+ service (path: string, service: _dependency.Service): _image.Image
9
11
 
10
- service(path: string, service: _dependency.Service): _image.Image
12
+ prepare (path: string): Promise<string>
11
13
 
12
- prepare(path: string): Promise<string>
14
+ build (): Promise<void>
13
15
 
14
- build(): Promise<void>
16
+ push (): Promise<void>
15
17
 
16
- push(): Promise<void>
18
+ tags (): string[]
17
19
  }
18
20
 
19
21
  }
@@ -1,4 +1,4 @@
1
- import type * as _deployment from './deployment'
1
+ import type * as _deployment from './deployment.js'
2
2
 
3
3
  declare namespace toa.deployment {
4
4
 
@@ -11,6 +11,9 @@ declare namespace toa.deployment {
11
11
  interface Service extends _deployment.Deployable {
12
12
  port: number
13
13
  ingress?: Ingress
14
+
15
+ /** Annotations for the Service itself, as opposed to `ingress.annotations`. */
16
+ annotations?: object
14
17
  }
15
18
 
16
19
  }
@@ -0,0 +1,73 @@
1
+ import type { Manifest } from '@toa.io/norm'
2
+ import type { Locator } from '@toa.io/core'
3
+
4
+ export interface Service {
5
+ group: string
6
+ name: string
7
+ version: string
8
+ port?: number
9
+ ingress?: Ingress
10
+ /** Annotations for the Service itself, as opposed to `ingress.annotations`. */
11
+ annotations?: object
12
+ resources?: Resources
13
+ variables?: Variable[]
14
+ components?: string[]
15
+ probe?: Probe | false
16
+ }
17
+
18
+ export interface Variable {
19
+ name: string
20
+ value?: string
21
+ secret?: {
22
+ name: string
23
+ key: string
24
+ optional?: boolean
25
+ }
26
+ }
27
+
28
+ export interface Instance<T> {
29
+ locator: Locator
30
+ manifest: T
31
+ component: Manifest
32
+ }
33
+
34
+ export type Instances<T> = Array<Instance<T>>
35
+
36
+ export type Variables = Record<'global' | string, Variable[]>
37
+ export type Mounts = Record<'global' | string, Mount[]>
38
+
39
+ export interface Dependency {
40
+ services?: Service[]
41
+ variables?: Variables
42
+ mounts?: Mounts
43
+ /** Event labels (`namespace.name.event`) this dependency consumes. */
44
+ events?: string[]
45
+ /** Default probe for compositions and services without their own probe. `false` disables. */
46
+ probe?: Probe | false
47
+ }
48
+
49
+ interface Ingress {
50
+ /** Path prefix this service claims on the host. Defaults to `/`. */
51
+ path?: string
52
+ default?: boolean
53
+ hosts?: string[]
54
+ class?: string
55
+ annotations?: object
56
+ }
57
+
58
+ export interface Probe {
59
+ port: number
60
+ path: string
61
+ delay?: number
62
+ }
63
+
64
+ interface Mount {
65
+ name: string
66
+ path: string
67
+ claim: string
68
+ }
69
+
70
+ export interface Resources {
71
+ cpu: string[]
72
+ memory: string[]
73
+ }
package/types/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './dependency.js'
@@ -1,39 +0,0 @@
1
- export type Service = {
2
- group: string
3
- name: string
4
- version: string
5
- port: number
6
- ingress: Ingress
7
- variables: Variable[]
8
- components?: string[]
9
- probe?: Probe
10
- }
11
-
12
- export type Variable = {
13
- name: string
14
- value?: string
15
- secret?: {
16
- name: string,
17
- key: string
18
- optional?: boolean
19
- }
20
- }
21
-
22
- export type Variables = Record<'global' | string, Variable[]>
23
-
24
- export type Dependency = {
25
- services?: Service[]
26
- variables?: Variables
27
- }
28
-
29
- type Ingress = {
30
- hosts: string[]
31
- class?: string
32
- annotations?: object
33
- }
34
-
35
- interface Probe {
36
- port: number
37
- path: string
38
- delay?: number
39
- }
package/types/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export * from './dependency'