@toa.io/operations 1.0.0-alpha.3 → 1.0.0-alpha.300

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 +131 -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 +103 -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 +148 -11
  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 +98 -8
  27. package/src/deployment/chart/values.yaml +29 -2
  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 +166 -36
  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 -49
  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 +16 -17
  43. package/src/deployment/images/image.js +83 -31
  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 +206 -40
  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 -30
  75. package/types/index.d.ts +0 -1
@@ -1,12 +1,15 @@
1
- 'use strict'
1
+ import { createRequire } from 'node:module'
2
+ import { join, dirname } from 'node:path'
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
4
 
3
- const { join, dirname } = require('node:path')
5
+ import { Image } from './image.js'
6
+ import { cp, writeFile } from 'node:fs/promises'
4
7
 
5
- const { Image } = require('./image')
6
- const fs = require('fs-extra')
8
+ // a service is named the way a package is, and its directory is where it lives
9
+ const require = createRequire(import.meta.url)
7
10
 
8
- class Service extends Image {
9
- dockerfile = join(__dirname, 'service.Dockerfile')
11
+ export class Service extends Image {
12
+ dockerfile = join(import.meta.dirname, 'service.Dockerfile')
10
13
 
11
14
  /**
12
15
  * Used by Dockerfile
@@ -35,7 +38,7 @@ class Service extends Image {
35
38
  * @param {string} reference
36
39
  * @param {toa.deployment.dependency.Service} service
37
40
  */
38
- constructor (scope, runtime, registry, reference, service) {
41
+ constructor(scope, runtime, registry, reference, service) {
39
42
  super(scope, runtime, registry)
40
43
 
41
44
  this.service = service.name
@@ -46,29 +49,75 @@ class Service extends Image {
46
49
  this.#version = service.version
47
50
  }
48
51
 
49
- get name () {
52
+ get name() {
50
53
  return 'extension-' + this.#group + '-' + this.#name
51
54
  }
52
55
 
53
- get version () {
56
+ get version() {
54
57
  return this.#version
55
58
  }
56
59
 
57
- async prepare (root) {
60
+ async prepare(root) {
58
61
  const context = await super.prepare(root)
59
62
 
60
- await fs.copy(this.#path, context)
63
+ await cp(this.#path, context, { recursive: true })
64
+ await writeFile(join(context, PACKAGES), this.#packages().join('\n'))
61
65
 
62
66
  return context
63
67
  }
68
+
69
+ /**
70
+ * What the components this service runs declare, as `name@version`.
71
+ *
72
+ * Each is installed beside its component, where the component's own modules import it, and
73
+ * beside Toa, where an extension that imports on a component's behalf is — a storage
74
+ * provider's SDK is imported by `@toa.io/extensions.storages`, not by the component that
75
+ * declares the storage.
76
+ *
77
+ * @returns {string[]}
78
+ */
79
+ #packages() {
80
+ const directory = join(this.#path, 'components')
81
+
82
+ if (!existsSync(directory)) return []
83
+
84
+ /** @type {Record<string, string>} */
85
+ const packages = {}
86
+
87
+ for (const label of readdirSync(directory)) {
88
+ const manifest = join(directory, label, 'package.json')
89
+
90
+ if (!existsSync(manifest)) continue
91
+
92
+ Object.assign(packages, JSON.parse(readFileSync(manifest, 'utf8')).dependencies)
93
+ }
94
+
95
+ return Object.entries(packages)
96
+ .map(([name, version]) => `${name}@${version}`)
97
+ .sort()
98
+ }
64
99
  }
65
100
 
101
+ /** Where the install reads what the components need, one `name@version` per line. */
102
+ const PACKAGES = '.packages'
103
+
66
104
  /**
105
+ * Where the extension is installed, which is what its image is built from. A deploy install
106
+ * carries what an extension declares and not the extension, so a service is built only where
107
+ * the runtime is installed beside the deployment library; elsewhere its image is published.
108
+ *
67
109
  * @param {string} reference
68
110
  * @returns {string}
69
111
  */
70
112
  const find = (reference) => {
71
- return dirname(require.resolve(join(reference, 'package.json')))
113
+ try {
114
+ return dirname(require.resolve(join(reference, 'package.json')))
115
+ } catch (error) {
116
+ throw new Error(
117
+ `'${reference}' is not installed, and \`registry.services: build\` builds its image ` +
118
+ 'from where it is: install the runtime beside @toa.io/operations, or take the ' +
119
+ 'image the release publishes with `registry.services: published`',
120
+ { cause: error }
121
+ )
122
+ }
72
123
  }
73
-
74
- exports.Service = Service
@@ -1,5 +1 @@
1
- 'use strict'
2
-
3
- const { Factory } = require('./factory')
4
-
5
- exports.Factory = Factory
1
+ export { Factory } from './factory.js'
@@ -1,24 +1,27 @@
1
- 'use strict'
1
+ import * as workspace from './workspace.js'
2
2
 
3
- const workspace = require('./workspace')
4
-
5
- class Operator {
3
+ export class Operator {
6
4
  /** @type {toa.deployment.Deployment} */
7
5
  #deployment
8
6
 
9
7
  /** @type {toa.deployment.Registry} */
10
8
  #registry
11
9
 
10
+ /** @type {string | undefined} */
11
+ #environment
12
+
12
13
  /**
13
- * @param deployment {toa.deployment.Deployment}
14
- * @param registry {toa.deployment.Registry}
14
+ * @param {toa.deployment.Deployment} deployment
15
+ * @param {toa.deployment.Registry} registry
16
+ * @param {string} [environment]
15
17
  */
16
- constructor (deployment, registry) {
18
+ constructor(deployment, registry, environment) {
17
19
  this.#deployment = deployment
18
20
  this.#registry = registry
21
+ this.#environment = environment
19
22
  }
20
23
 
21
- async export (path) {
24
+ async export(path) {
22
25
  const target = await workspace.create('deployment', path)
23
26
 
24
27
  await this.#deployment.export(target)
@@ -26,33 +29,36 @@ class Operator {
26
29
  return target
27
30
  }
28
31
 
29
- async prepare (path) {
32
+ async prepare(path) {
30
33
  return await this.#registry.prepare(path)
31
34
  }
32
35
 
33
- async push () {
36
+ async push() {
34
37
  await this.#registry.push()
35
38
  }
36
39
 
37
- async install (options = {}) {
40
+ async install(options = {}) {
38
41
  options = Object.assign({}, OPTIONS, options)
39
42
 
40
43
  await Promise.all([this.export(), this.push()])
44
+ await this.#registry.alias(this.#environment)
41
45
  await this.#deployment.install(options)
42
46
  }
43
47
 
44
- async template (options = {}) {
48
+ async template(options = {}) {
45
49
  await this.export()
46
50
 
47
51
  return await this.#deployment.template(options)
48
52
  }
49
53
 
50
- variables () {
51
- return this.#deployment.variables()
54
+ variables(options) {
55
+ return this.#deployment.variables(options)
56
+ }
57
+
58
+ tags() {
59
+ return this.#registry.tags()
52
60
  }
53
61
  }
54
62
 
55
63
  /** @type {toa.deployment.installation.Options} */
56
64
  const OPTIONS = { wait: false }
57
-
58
- exports.Operator = Operator
@@ -1,10 +1,11 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { Operator } = require('../../src/deployment/operator')
4
- const { generate } = require('randomstring')
4
+ import { Operator } from '../../src/deployment/operator.js'
5
+ import { generate } from 'randomstring'
5
6
 
6
7
  it('should be', async () => {
7
- expect(Operator).toBeInstanceOf(Function)
8
+ assert.ok(Operator instanceof Function)
8
9
  })
9
10
 
10
11
  /** @type {toa.deployment.Operator} */
@@ -25,7 +26,7 @@ describe('env', () => {
25
26
  it('should be', async () => {
26
27
  operator = new Operator(deployment, registry)
27
28
 
28
- expect(operator.variables).toBeInstanceOf(Function)
29
+ assert.ok(operator.variables instanceof Function)
29
30
  })
30
31
 
31
32
  it('should return variables', async () => {
@@ -33,12 +34,61 @@ describe('env', () => {
33
34
 
34
35
  deployment.variables =
35
36
  /** @type {typeof toa.deployment.Operator.variables} */
36
- jest.fn(() => variables)
37
+ mock.fn(() => variables)
37
38
 
38
39
  operator = new Operator(deployment, registry)
39
40
 
40
41
  const output = operator.variables()
41
42
 
42
- expect(output).toStrictEqual(variables)
43
+ assert.deepStrictEqual(output, variables)
44
+ })
45
+ })
46
+
47
+ describe('install', () => {
48
+ /** @type {toa.deployment.Deployment} */
49
+ let deployment
50
+
51
+ /** @type {toa.deployment.Registry} */
52
+ let registry
53
+
54
+ beforeEach(() => {
55
+ deployment = /** @type {toa.deployment.Deployment} */ {
56
+ export: mock.fn(async () => {}),
57
+ install: mock.fn(async () => {})
58
+ }
59
+ registry = /** @type {toa.deployment.Registry} */ {
60
+ push: mock.fn(async () => {}),
61
+ alias: mock.fn(async () => {})
62
+ }
63
+ })
64
+
65
+ it('should tag the environment after a push', async () => {
66
+ const order = []
67
+
68
+ registry.push = mock.fn(async () => {
69
+ order.push('push')
70
+ })
71
+ registry.alias = mock.fn(async () => {
72
+ order.push('alias')
73
+ })
74
+ deployment.install = mock.fn(async () => {
75
+ order.push('install')
76
+ })
77
+
78
+ operator = new Operator(deployment, registry, 'production')
79
+
80
+ await operator.install()
81
+
82
+ assert.deepStrictEqual(order, ['push', 'alias', 'install'])
83
+ assert.deepStrictEqual(registry.alias.mock.calls[0].arguments, ['production'])
84
+ })
85
+
86
+ it('should not tag the environment on push', async () => {
87
+ operator = new Operator(deployment, registry, 'production')
88
+
89
+ await operator.push()
90
+
91
+ assert.strictEqual(registry.push.mock.callCount(), 1)
92
+ assert.strictEqual(registry.alias.mock.callCount(), 0)
43
93
  })
44
94
  })
@@ -1,59 +1,83 @@
1
- 'use strict'
2
-
3
- const workspace = require('./workspace')
1
+ import * as workspace from './workspace.js'
4
2
 
5
3
  /**
6
4
  * @implements {toa.deployment.Registry}
7
5
  */
8
- class Registry {
6
+ export class Registry {
9
7
  #registry
10
8
 
11
9
  #factory
12
10
 
13
11
  #process
14
12
 
13
+ /** @type {toa.deployment.images.Image[]} */
15
14
  #images = []
16
15
 
17
- constructor (registry, factory, process) {
16
+ /** @type {Promise<string> | undefined} */
17
+ #builder
18
+
19
+ constructor(registry, factory, process) {
18
20
  this.#registry = registry
19
21
  this.#factory = factory
20
22
  this.#process = process
21
23
  }
22
24
 
23
- composition (composition) {
25
+ composition(composition) {
24
26
  return this.#create('composition', composition)
25
27
  }
26
28
 
27
- service (path, service) {
29
+ service(path, service) {
28
30
  return this.#create('service', path, service)
29
31
  }
30
32
 
31
- async prepare (root) {
33
+ mono(composition) {
34
+ return this.#create('mono', composition)
35
+ }
36
+
37
+ async prepare(root) {
32
38
  const path = await workspace.create('images', root)
33
39
 
34
- await Promise.all(this.#images.map((image) => image.prepare(path)))
40
+ await Promise.all(this.#all().map((image) => image.prepare(path)))
35
41
 
36
42
  return path
37
43
  }
38
44
 
39
- async build () {
40
- await this.prepare()
45
+ async build() {
46
+ await this.#run(false)
47
+ }
41
48
 
42
- for (const image of this.#images) await this.#build(image)
49
+ async push() {
50
+ await this.#run(true)
43
51
  }
44
52
 
45
- async push () {
46
- await this.prepare()
53
+ /**
54
+ * A second tag on each workload image, the environment, so a retention job can see what
55
+ * a deploy left running. `deps-*` is not in `#images`.
56
+ *
57
+ * @param {string} [environment]
58
+ * @returns {Promise<void>}
59
+ */
60
+ async alias(environment) {
61
+ if (
62
+ typeof environment !== 'string' ||
63
+ environment === '' ||
64
+ CONTENT_TAG.test(environment)
65
+ )
66
+ return
67
+
68
+ await Promise.all(this.#images.map((image) => this.#alias(image, environment)))
69
+ }
47
70
 
48
- for (const image of this.#images) await this.#push(image)
71
+ tags() {
72
+ return this.#images.map((image) => image.reference)
49
73
  }
50
74
 
51
75
  /**
52
- * @param {'composition' | 'service'} type
76
+ * @param {'composition' | 'service' | 'mono'} type
53
77
  * @param {...any} args
54
78
  * @returns {toa.deployment.images.Image}
55
79
  */
56
- #create (type, ...args) {
80
+ #create(type, ...args) {
57
81
  const image = this.#factory[type](...args)
58
82
 
59
83
  this.#images.push(image)
@@ -61,31 +85,91 @@ class Registry {
61
85
  return image
62
86
  }
63
87
 
88
+ /**
89
+ * Every image and what it is laid over.
90
+ *
91
+ * @returns {toa.deployment.images.Image[]}
92
+ */
93
+ #all() {
94
+ return this.#images.flatMap((image) =>
95
+ image.dependencies === undefined ? [image] : [image.dependencies, image]
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Every image is probed at once, and what is missing is prepared and built concurrently:
101
+ * a build is bound by the registry it uploads to, not by the runner, so one at a time
102
+ * leaves both idle. What an image is laid over is built before it.
103
+ *
104
+ * @param {boolean} push
105
+ * @returns {Promise<void>}
106
+ */
107
+ async #run(push) {
108
+ // a push builds on the container driver, whose bootstrap is spent while the probes are out
109
+ if (push) this.#ensureBuilder().catch(() => undefined)
110
+
111
+ const images = this.#all()
112
+
113
+ const existing = await Promise.all(
114
+ images.map((image) => this.exists(image.reference))
115
+ )
116
+
117
+ const missing = images.filter((image, index) => {
118
+ if (existing[index]) console.log('Image already exists, skipping:', image.reference)
119
+
120
+ return !existing[index]
121
+ })
122
+
123
+ if (missing.length === 0) return
124
+
125
+ const path = await workspace.create('images')
126
+
127
+ await Promise.all(missing.map((image) => image.prepare(path)))
128
+
129
+ const bases = new Set(this.#images.map((image) => image.dependencies))
130
+ const build = (image) => this.#build(image, push)
131
+
132
+ await pool(
133
+ missing.filter((image) => bases.has(image)),
134
+ build
135
+ )
136
+ await pool(
137
+ missing.filter((image) => !bases.has(image)),
138
+ build
139
+ )
140
+ }
141
+
64
142
  /**
65
143
  * @param {toa.deployment.images.Image} image
66
144
  * @param {boolean} [push]
67
145
  * @returns {Promise<void>}
68
146
  */
69
- async #build (image, push = false) {
147
+ async #build(image, push = false) {
70
148
  const args = ['--context=default', 'buildx', 'build']
71
149
 
72
150
  if (push) args.push('--push')
151
+ else args.push('--load')
73
152
 
74
153
  args.push('--tag', image.reference, image.context)
75
154
 
76
- const multiarch = this.#registry.platforms !== null
77
-
78
- if (this.#registry.build?.arguments !== undefined) {
79
- for (const arg of this.#registry.build.arguments) args.push('--build-arg', `${arg}=${process.env[arg]}`)
155
+ if (image.arguments && this.#registry.build?.arguments !== undefined) {
156
+ for (const arg of this.#registry.build.arguments)
157
+ args.push('--build-arg', `${arg}=${process.env[arg]}`)
80
158
  }
81
159
 
82
- if (multiarch) {
83
- const platform = this.#registry.platforms.join(',')
160
+ const platforms = this.#registry.platforms
161
+
162
+ if (platforms !== null) args.push('--platform', platforms.join(','))
84
163
 
85
- args.push('--platform', platform)
86
- args.push('--builder', BUILDER)
164
+ // a push takes the container driver: its registry exporter is what lays an image over a
165
+ // base it never pulled. A load takes the daemon's own, unless it builds for a platform
166
+ // the runner is not, which the daemon's cannot
167
+ if (push || (platforms !== null && platforms.length > 1)) {
168
+ args.push('--builder', await this.#ensureBuilder())
87
169
 
88
- await this.#ensureBuilder()
170
+ // the driver that produces attestations is this one, and nothing reads them;
171
+ // each is a manifest list to export and push per image
172
+ args.push('--provenance=false')
89
173
  } else args.push('--builder', 'default')
90
174
 
91
175
  args.push('--progress', 'plain')
@@ -93,27 +177,109 @@ class Registry {
93
177
  await this.#process.execute('docker', args)
94
178
  }
95
179
 
96
- async #push (image) {
97
- await this.#build(image, true)
180
+ /**
181
+ * @param {toa.deployment.images.Image} image
182
+ * @param {string} environment
183
+ * @returns {Promise<void>}
184
+ */
185
+ async #alias(image, environment) {
186
+ const target = retag(image.reference, environment)
187
+ const args = ['buildx', 'imagetools', 'create', '--tag', target]
188
+
189
+ if (LOCAL.test(image.reference)) args.push('--insecure')
190
+
191
+ args.push(image.reference)
192
+
193
+ await this.#process.execute('docker', args)
98
194
  }
99
195
 
100
- async #ensureBuilder () {
101
- const ls = 'buildx ls'.split(' ')
102
- const output = await this.#process.execute('docker', ls, { silently: true })
103
- const exists = output.split('\n').findIndex((line) => line.startsWith('toa '))
196
+ async exists(tag) {
197
+ const args = ['manifest', 'inspect']
198
+
199
+ // a registry on this machine speaks plain HTTP, which the probe has to be told
200
+ if (LOCAL.test(tag)) args.push('--insecure')
104
201
 
105
- if (exists === -1) await this.#createBuilder()
202
+ args.push(tag)
203
+
204
+ try {
205
+ await this.#process.execute('docker', args, { silently: true })
206
+ } catch (error) {
207
+ console.log(error.message)
208
+
209
+ return false
210
+ }
211
+
212
+ return true
106
213
  }
107
214
 
108
- async #createBuilder () {
109
- const create = `buildx create --name ${BUILDER} --use`.split(' ')
110
- const bootstrap = 'buildx inspect --bootstrap'.split(' ')
215
+ /** @returns {Promise<string>} */
216
+ #ensureBuilder() {
217
+ // the promise is what is memoized, not its value: concurrent builds ask before any answers
218
+ this.#builder ??= this.#createBuilder()
111
219
 
112
- await this.#process.execute('docker', create)
113
- await this.#process.execute('docker', bootstrap)
220
+ return this.#builder
114
221
  }
222
+
223
+ /** @returns {Promise<string>} */
224
+ async #createBuilder() {
225
+ try {
226
+ await this.#process.execute('docker', ['buildx', 'inspect', BUILDER], {
227
+ silently: true
228
+ })
229
+ } catch {
230
+ // on the host's network, a registry the host reaches as `localhost` is reached
231
+ await this.#process.execute('docker', [
232
+ 'buildx',
233
+ 'create',
234
+ '--name',
235
+ BUILDER,
236
+ '--driver',
237
+ 'docker-container',
238
+ '--driver-opt',
239
+ 'network=host',
240
+ '--bootstrap'
241
+ ])
242
+ }
243
+
244
+ return BUILDER
245
+ }
246
+ }
247
+
248
+ /**
249
+ * @template T
250
+ * @param {T[]} items
251
+ * @param {(item: T) => Promise<void>} work
252
+ * @returns {Promise<void>}
253
+ */
254
+ async function pool(items, work) {
255
+ const queue = items.slice()
256
+
257
+ const workers = Array.from(
258
+ { length: Math.min(CONCURRENCY, queue.length) },
259
+ async () => {
260
+ while (queue.length > 0) await work(queue.shift())
261
+ }
262
+ )
263
+
264
+ await Promise.all(workers)
265
+ }
266
+
267
+ /**
268
+ * @param {string} reference
269
+ * @param {string} tag
270
+ * @returns {string}
271
+ */
272
+ function retag(reference, tag) {
273
+ return reference.slice(0, reference.lastIndexOf(':') + 1) + tag
115
274
  }
116
275
 
117
276
  const BUILDER = 'toa'
118
277
 
119
- exports.Registry = Registry
278
+ /** An environment named this way is a content tag, and must not be moved as one. */
279
+ const CONTENT_TAG = /^(deps-)?[0-9a-f]{8}$/
280
+
281
+ /** A reference into a registry on this machine, by any of the names it goes by. */
282
+ const LOCAL = /^(localhost|127\.\d+\.\d+\.\d+|host\.docker\.internal)(:\d+)?\//
283
+
284
+ /** How many builds run at once. Past this the uploads contend for the same uplink. */
285
+ const CONCURRENCY = 3