@toa.io/operations 1.0.0-alpha.26 → 1.0.0-alpha.261

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 (33) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/image/runtime.Dockerfile +4 -0
  3. package/package.json +8 -8
  4. package/readme.md +16 -0
  5. package/src/deployment/.deployment/.describe/compositions.js +7 -2
  6. package/src/deployment/.deployment/.describe/mounts.js +24 -0
  7. package/src/deployment/.deployment/.describe/services.js +6 -1
  8. package/src/deployment/.deployment/.describe/variables.js +6 -5
  9. package/src/deployment/.deployment/describe.js +60 -5
  10. package/src/deployment/.deployment/merge.js +8 -1
  11. package/src/deployment/chart/templates/compositions.yaml +70 -0
  12. package/src/deployment/chart/templates/mono.yaml +168 -0
  13. package/src/deployment/chart/templates/services.yaml +67 -2
  14. package/src/deployment/chart/values.yaml +11 -1
  15. package/src/deployment/composition.js +1 -0
  16. package/src/deployment/deployment.js +6 -2
  17. package/src/deployment/factory.js +26 -8
  18. package/src/deployment/images/composition.Dockerfile +2 -2
  19. package/src/deployment/images/composition.js +16 -6
  20. package/src/deployment/images/factory.js +12 -3
  21. package/src/deployment/images/image.js +16 -7
  22. package/src/deployment/images/mono.Dockerfile +18 -0
  23. package/src/deployment/images/mono.js +82 -0
  24. package/src/deployment/images/runtime-base.test.js +90 -0
  25. package/src/deployment/images/service.Dockerfile +3 -3
  26. package/src/deployment/operator.js +4 -0
  27. package/src/deployment/registry.js +64 -16
  28. package/src/deployment/registry.test.js +175 -0
  29. package/types/_deployment/images/image.d.ts +4 -2
  30. package/types/_deployment/registry.d.ts +9 -7
  31. package/types/dependency.ts +67 -0
  32. package/types/dependency.d.ts +0 -39
  33. /package/types/{index.d.ts → index.ts} +0 -0
@@ -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,7 +25,6 @@ compositions:
18
25
  replicas: 3
19
26
  components:
20
27
  - users-users
21
- # TODO: create component services only if sync binding is being used
22
28
  components:
23
29
  - todos-tasks
24
30
  - todos-stats
@@ -28,7 +34,11 @@ services:
28
34
  image: localhost:5000/resources-gateway:0.0.0
29
35
  port: 8000
30
36
  replicas: 2
37
+ resources:
38
+ cpu: [100m, 1]
39
+ memory: [100Mi, 1Gi]
31
40
  ingress:
41
+ default: true
32
42
  hosts: [dummies.toa.io]
33
43
  class: alb
34
44
  annotations:
@@ -10,6 +10,7 @@ class Composition {
10
10
  this.name = composition.name
11
11
  this.image = image.reference
12
12
  this.components = composition.components.map(component)
13
+ this.resources = composition.resources
13
14
  }
14
15
  }
15
16
 
@@ -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
 
@@ -42,6 +42,7 @@ class Deployment {
42
42
 
43
43
  if (options.namespace !== undefined) args.push('-n', options.namespace)
44
44
  if (options.wait === true) args.push('--wait')
45
+ if (options.timeout !== undefined) args.push('--timeout', options.timeout)
45
46
 
46
47
  await this.#process.execute('helm', ['dependency', 'update', this.#target])
47
48
  await this.#process.execute('helm', ['upgrade', this.#chart.name, '-i', ...args, this.#target])
@@ -68,6 +69,9 @@ class Deployment {
68
69
  addVariables(this.#values.compositions, variables, used)
69
70
  addVariables(this.#values.services, variables, used)
70
71
 
72
+ if (this.#values.mono !== undefined)
73
+ addVariables([this.#values.mono], variables, used)
74
+
71
75
  return variables
72
76
  }
73
77
  }
@@ -11,25 +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
18
- #extensionComponents = []
20
+ #image
19
21
 
20
- constructor (context) {
22
+ constructor (context, options = {}) {
21
23
  this.#context = context
24
+ this.#root = options.root
25
+ this.#mono = options.mono === true
22
26
  this.#process = new Process()
23
27
 
24
28
  const imagesFactory = new ImagesFactory(context.name, context.runtime, context.registry)
25
29
 
26
- this.#registry = new Registry(context.registry, imagesFactory, this.#process)
27
- this.#compositions = context.compositions.map((composition) => this.#composition(composition))
30
+ this.#registry = new Registry(context.name, context.registry, imagesFactory, this.#process)
28
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))
29
40
  }
30
41
 
31
42
  operator () {
32
- 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
+ )
33
50
 
34
51
  return new Operator(deployment, this.#registry)
35
52
  }
@@ -71,7 +88,8 @@ class Factory {
71
88
  const dependency = module.deployment(instances, annotation)
72
89
 
73
90
  /** @type {toa.deployment.Service[]} */
74
- 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))
75
93
 
76
94
  return { ...dependency, services }
77
95
  }
@@ -87,10 +105,10 @@ class Factory {
87
105
  return new Service(service, image)
88
106
  }
89
107
 
90
- static async create (path, environment) {
108
+ static async create (path, environment, options = {}) {
91
109
  const context = await load(path, environment)
92
110
 
93
- return new Factory(context)
111
+ return new Factory(context, { ...options, root: path })
94
112
  }
95
113
  }
96
114
 
@@ -5,7 +5,6 @@ FROM {{build.image}}
5
5
  ENV NODE_ENV=production
6
6
  RUN if [ "{{runtime.registry}}" != "" ]; then npm set registry {{runtime.registry}}; fi
7
7
  RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
8
- RUN npm i -g @toa.io/runtime@{{runtime.version}} --omit=dev
9
8
 
10
9
  WORKDIR /composition
11
10
  COPY --chown=node:node . /composition
@@ -13,7 +12,8 @@ COPY --chown=node:node . /composition
13
12
  {{build.run}}
14
13
 
15
14
  # run 'npm i' in each component
16
- RUN for entry in *; do if [ -f "$entry/package.json" ]; then (cd $entry && npm i --omit=dev); fi; done
15
+ RUN --mount=type=cache,target=/root/.npm \
16
+ for entry in *; do if [ -f "$entry/package.json" ]; then (cd $entry && npm i --omit=dev); fi; done
17
17
 
18
18
  USER node
19
19
  CMD toa compose *
@@ -5,6 +5,7 @@ const fs = require('fs-extra')
5
5
  const { createHash } = require('node:crypto')
6
6
 
7
7
  const { Image } = require('./image')
8
+ const { undef } = require('@toa.io/concise/source/expressions/undefined')
8
9
 
9
10
  class Composition extends Image {
10
11
  dockerfile = join(__dirname, 'composition.Dockerfile')
@@ -37,11 +38,7 @@ class Composition extends Image {
37
38
  }
38
39
 
39
40
  get base () {
40
- if (this.#image !== undefined) {
41
- return this.#image
42
- }
43
-
44
- let image = null
41
+ let image = this.#image
45
42
 
46
43
  for (const component of this.#components) {
47
44
  const value = component.build?.image
@@ -53,7 +50,20 @@ class Composition extends Image {
53
50
  image = value
54
51
  }
55
52
 
56
- return image ?? undefined
53
+ return image
54
+ }
55
+
56
+ get run () {
57
+ const commands = []
58
+
59
+ for (const component of this.#components) {
60
+ const run = component.build?.run
61
+
62
+ if (run !== undefined)
63
+ commands.push(run)
64
+ }
65
+
66
+ return commands.join('\n')
57
67
  }
58
68
 
59
69
  async prepare (root) {
@@ -2,10 +2,8 @@
2
2
 
3
3
  const { Composition } = require('./composition')
4
4
  const { Service } = require('./service')
5
+ const { Mono } = require('./mono')
5
6
 
6
- /**
7
- * @implements {toa.deployment.images.Factory}
8
- */
9
7
  class Factory {
10
8
  /** @type {string} */
11
9
  #scope
@@ -48,6 +46,17 @@ class Factory {
48
46
 
49
47
  return instance
50
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
+ }
51
60
  }
52
61
 
53
62
  exports.Factory = Factory
@@ -26,9 +26,7 @@ class Image {
26
26
  #registry
27
27
  #runtime
28
28
  #values = {
29
- build: {
30
- image: 'node:20.9.0-alpine3.18'
31
- }
29
+ build: {}
32
30
  }
33
31
 
34
32
  constructor (scope, runtime, registry) {
@@ -54,6 +52,8 @@ class Image {
54
52
 
55
53
  get base () {}
56
54
 
55
+ get run () {}
56
+
57
57
  async prepare (root) {
58
58
  if (this.dockerfile === undefined) throw new Error('Dockerfile isn\'t specified')
59
59
 
@@ -65,7 +65,7 @@ class Image {
65
65
 
66
66
  const template = await read(this.dockerfile, 'utf-8')
67
67
  const contents = template.replace(/{{(\S{1,32})}}/g, (_, key) => this.#value(key))
68
- const ignore = 'Dockerfile'
68
+ const ignore = ['Dockerfile', '**/node_modules'].join('\r\n')
69
69
 
70
70
  await write(join(path, 'Dockerfile'), contents)
71
71
  await write(join(path, '.dockerignore'), ignore)
@@ -77,13 +77,19 @@ class Image {
77
77
 
78
78
  #setValues () {
79
79
  this.#values.runtime = this.#runtime
80
- this.#values.build = overwrite(this.#values.build, this.#registry.build)
80
+ this.#values.build = overwrite({
81
+ image: `${RUNTIME_IMAGE}:${this.#runtime.version}`
82
+ }, this.#registry.build)
81
83
 
82
84
  const image = this.base
83
85
 
84
- if (image !== undefined) {
86
+ if (image !== undefined)
85
87
  this.#values.build.image = image
86
- }
88
+
89
+ const run = this.run
90
+
91
+ if (run !== undefined)
92
+ this.#values.build.run = (this.#values.build.run === undefined ? '' : this.#values.build.run + '\n') + run
87
93
 
88
94
  if (this.#values.build.arguments !== undefined) this.#values.build.arguments = createArguments(this.#values.build.arguments)
89
95
  if (this.#values.build.run !== undefined) this.#values.build.run = createRunCommands(this.#values.build.run)
@@ -121,4 +127,7 @@ function createArguments (variables) {
121
127
  return args.join('\n')
122
128
  }
123
129
 
130
+ const RUNTIME_IMAGE = 'ghcr.io/toa-io/runtime'
131
+
124
132
  exports.Image = Image
133
+ exports.RUNTIME_IMAGE = RUNTIME_IMAGE
@@ -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
@@ -0,0 +1,90 @@
1
+ 'use strict'
2
+
3
+ const { join } = require('node:path')
4
+ const { readFile } = require('node:fs/promises')
5
+ const { directory } = require('@toa.io/filesystem')
6
+
7
+ const { Image, RUNTIME_IMAGE } = require('./image')
8
+
9
+ const compositionDockerfile = join(__dirname, 'composition.Dockerfile')
10
+ const serviceDockerfile = join(__dirname, 'service.Dockerfile')
11
+
12
+ class TestImage extends Image {
13
+ dockerfile
14
+
15
+ #name
16
+ #base
17
+
18
+ constructor (runtime, registry, dockerfile, name = 'test', base) {
19
+ super('acme', runtime, registry)
20
+
21
+ this.dockerfile = dockerfile
22
+ this.#name = name
23
+ this.#base = base
24
+ }
25
+
26
+ get name () {
27
+ return this.#name
28
+ }
29
+
30
+ get version () {
31
+ return 'abcdef12'
32
+ }
33
+
34
+ get base () {
35
+ return this.#base
36
+ }
37
+ }
38
+
39
+ describe('runtime base image', () => {
40
+ /** @type {string} */
41
+ let root
42
+
43
+ beforeEach(async () => {
44
+ root = await directory.temp('toa-runtime-base-test')
45
+ })
46
+
47
+ it('should default build.image to version-pinned GHCR runtime image', async () => {
48
+ const runtime = { version: '1.0.0-alpha.232' }
49
+ const image = new TestImage(runtime, {}, compositionDockerfile)
50
+
51
+ const path = await image.prepare(root)
52
+ const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
53
+
54
+ expect(dockerfile).toContain(`FROM ${RUNTIME_IMAGE}:1.0.0-alpha.232`)
55
+ expect(dockerfile).not.toMatch(/npm i -g @toa\.io\/runtime/)
56
+ })
57
+
58
+ it('should not install runtime in service Dockerfile template', async () => {
59
+ const runtime = { version: '1.0.0-alpha.99' }
60
+ const image = new TestImage(runtime, {}, serviceDockerfile, 'service-test')
61
+
62
+ const path = await image.prepare(root)
63
+ const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
64
+
65
+ expect(dockerfile).toContain(`FROM ${RUNTIME_IMAGE}:1.0.0-alpha.99`)
66
+ expect(dockerfile).not.toMatch(/npm i -g @toa\.io\/runtime/)
67
+ })
68
+
69
+ it('should allow registry.build.image override', async () => {
70
+ const runtime = { version: '1.0.0-alpha.232' }
71
+ const registry = { build: { image: 'node:24.14.0-alpine3.22' } }
72
+ const image = new TestImage(runtime, registry, compositionDockerfile)
73
+
74
+ const path = await image.prepare(root)
75
+ const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
76
+
77
+ expect(dockerfile).toContain('FROM node:24.14.0-alpine3.22')
78
+ expect(dockerfile).not.toContain(RUNTIME_IMAGE)
79
+ })
80
+
81
+ it('should allow composition.image override via base', async () => {
82
+ const runtime = { version: '1.0.0-alpha.232' }
83
+ const image = new TestImage(runtime, {}, compositionDockerfile, 'mono', 'custom.example/base:1')
84
+
85
+ const path = await image.prepare(root)
86
+ const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
87
+
88
+ expect(dockerfile).toContain('FROM custom.example/base:1')
89
+ })
90
+ })
@@ -1,14 +1,14 @@
1
- FROM node:20.9.0-alpine3.18
1
+ FROM {{build.image}}
2
2
 
3
3
  ENV NODE_ENV=production
4
4
  RUN if [ "{{runtime.registry}}" != "" ]; then npm set registry {{runtime.registry}}; fi
5
5
  RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
6
- RUN npm i -g @toa.io/runtime@{{runtime.version}} --omit=dev
7
6
 
8
7
  WORKDIR /service
9
8
  COPY --chown=node:node . /service
10
9
 
11
- RUN npm i --omit=dev
10
+ RUN --mount=type=cache,target=/root/.npm \
11
+ npm i --omit=dev
12
12
 
13
13
  USER node
14
14
  CMD toa serve .
@@ -50,6 +50,10 @@ class Operator {
50
50
  variables () {
51
51
  return this.#deployment.variables()
52
52
  }
53
+
54
+ tags () {
55
+ return this.#registry.tags()
56
+ }
53
57
  }
54
58
 
55
59
  /** @type {toa.deployment.installation.Options} */
@@ -1,12 +1,14 @@
1
1
  'use strict'
2
2
 
3
+ const { posix } = require('node:path')
3
4
  const workspace = require('./workspace')
4
- const { newid } = require('@toa.io/generic')
5
5
 
6
6
  /**
7
7
  * @implements {toa.deployment.Registry}
8
8
  */
9
9
  class Registry {
10
+ #scope
11
+
10
12
  #registry
11
13
 
12
14
  #factory
@@ -15,7 +17,11 @@ class Registry {
15
17
 
16
18
  #images = []
17
19
 
18
- constructor (registry, factory, process) {
20
+ /** @type {string | undefined} */
21
+ #builder
22
+
23
+ constructor (scope, registry, factory, process) {
24
+ this.#scope = scope
19
25
  this.#registry = registry
20
26
  this.#factory = factory
21
27
  this.#process = process
@@ -29,6 +35,10 @@ class Registry {
29
35
  return this.#create('service', path, service)
30
36
  }
31
37
 
38
+ mono (composition, root) {
39
+ return this.#create('mono', composition, root)
40
+ }
41
+
32
42
  async prepare (root) {
33
43
  const path = await workspace.create('images', root)
34
44
 
@@ -40,9 +50,8 @@ class Registry {
40
50
  async build () {
41
51
  await this.prepare()
42
52
 
43
- for (const image of this.#images) {
53
+ for (const image of this.#images)
44
54
  await this.#build(image)
45
- }
46
55
  }
47
56
 
48
57
  async push () {
@@ -51,8 +60,12 @@ class Registry {
51
60
  for (const image of this.#images) await this.#push(image)
52
61
  }
53
62
 
63
+ tags () {
64
+ return this.#images.map((image) => image.reference)
65
+ }
66
+
54
67
  /**
55
- * @param {'composition' | 'service'} type
68
+ * @param {'composition' | 'service' | 'mono'} type
56
69
  * @param {...any} args
57
70
  * @returns {toa.deployment.images.Image}
58
71
  */
@@ -70,13 +83,17 @@ class Registry {
70
83
  * @returns {Promise<void>}
71
84
  */
72
85
  async #build (image, push = false) {
86
+ if (await this.exists(image.reference)) {
87
+ console.log('Image already exists, skipping:', image.reference)
88
+ return
89
+ }
90
+
73
91
  const args = ['--context=default', 'buildx', 'build']
74
92
 
75
- if (push) {
93
+ if (push)
76
94
  args.push('--push')
77
- } else {
95
+ else
78
96
  args.push('--load')
79
- }
80
97
 
81
98
  args.push('--tag', image.reference, image.context)
82
99
 
@@ -88,14 +105,15 @@ class Registry {
88
105
 
89
106
  if (multiarch) {
90
107
  const platform = this.#registry.platforms.join(',')
91
- const builder = await this.#createBuilder()
108
+ const builder = await this.#ensureBuilder()
92
109
 
93
110
  args.push('--platform', platform)
94
111
  args.push('--builder', builder)
95
112
 
96
- } else {
113
+ if (this.#registry.base !== undefined)
114
+ this.#appendCache(args)
115
+ } else
97
116
  args.push('--builder', 'default')
98
- }
99
117
 
100
118
  args.push('--progress', 'plain')
101
119
 
@@ -106,13 +124,43 @@ class Registry {
106
124
  await this.#build(image, true)
107
125
  }
108
126
 
109
- async #createBuilder () {
110
- const name = `toa-${newid()}`
111
- const create = `buildx create --name ${name} --bootstrap --use`.split(' ')
127
+ async exists (tag) {
128
+ const args = ['manifest', 'inspect', tag]
129
+
130
+ try {
131
+ await this.#process.execute('docker', args, { silently: true })
132
+ } catch (error) {
133
+ console.log(error.message)
134
+
135
+ return false
136
+ }
137
+
138
+ return true
139
+ }
140
+
141
+ async #ensureBuilder () {
142
+ if (this.#builder !== undefined)
143
+ return this.#builder
144
+
145
+ try {
146
+ await this.#process.execute('docker', ['buildx', 'inspect', BUILDER], { silently: true })
147
+ } catch {
148
+ await this.#process.execute('docker', ['buildx', 'create', '--name', BUILDER, '--bootstrap'])
149
+ }
150
+
151
+ this.#builder = BUILDER
152
+
153
+ return BUILDER
154
+ }
112
155
 
113
- await this.#process.execute('docker', create)
156
+ /**
157
+ * @param {string[]} args
158
+ */
159
+ #appendCache (args) {
160
+ const ref = posix.join(this.#registry.base, this.#scope, 'buildcache')
114
161
 
115
- return name
162
+ args.push('--cache-from', `type=registry,ref=${ref}`)
163
+ args.push('--cache-to', `type=registry,ref=${ref},mode=max,image-manifest=true`)
116
164
  }
117
165
  }
118
166