@toa.io/operations 1.0.0-alpha.31 → 1.0.0-alpha.310

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 (76) hide show
  1. package/CHANGELOG.md +213 -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 +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 +153 -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 +14 -0
  24. package/src/deployment/chart/templates/components.yaml +1 -1
  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 +147 -34
  31. package/src/deployment/drain.js +70 -0
  32. package/src/deployment/drain.test.js +64 -0
  33. package/src/deployment/factory.js +169 -37
  34. package/src/deployment/images/bundle.js +88 -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 +268 -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 +149 -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 +22 -16
  55. package/src/deployment/operator.test.js +57 -7
  56. package/src/deployment/registry.js +210 -46
  57. package/src/deployment/registry.test.js +539 -0
  58. package/src/deployment/service.js +4 -6
  59. package/src/deployment/workspace.js +13 -8
  60. package/src/index.js +3 -2
  61. package/src/process.js +51 -13
  62. package/src/process.test.js +33 -0
  63. package/types/_deployment/composition.d.ts +2 -3
  64. package/types/_deployment/dependency.d.ts +13 -7
  65. package/types/_deployment/deployment.d.ts +7 -8
  66. package/types/_deployment/factory.d.ts +2 -4
  67. package/types/_deployment/images/factory.d.ts +6 -8
  68. package/types/_deployment/images/image.d.ts +16 -1
  69. package/types/_deployment/images/registry.d.ts +8 -11
  70. package/types/_deployment/operator.d.ts +10 -9
  71. package/types/_deployment/registry.d.ts +7 -4
  72. package/types/_deployment/service.d.ts +4 -3
  73. package/types/dependency.ts +79 -0
  74. package/types/index.ts +1 -0
  75. package/types/dependency.d.ts +0 -39
  76. 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,62 +1,83 @@
1
- 'use strict'
2
-
3
- const workspace = require('./workspace')
4
- const { newid } = require('@toa.io/generic')
1
+ import * as workspace from './workspace.js'
5
2
 
6
3
  /**
7
4
  * @implements {toa.deployment.Registry}
8
5
  */
9
- class Registry {
6
+ export class Registry {
10
7
  #registry
11
8
 
12
9
  #factory
13
10
 
14
11
  #process
15
12
 
13
+ /** @type {toa.deployment.images.Image[]} */
16
14
  #images = []
17
15
 
18
- constructor (registry, factory, process) {
16
+ /** @type {Promise<string> | undefined} */
17
+ #builder
18
+
19
+ constructor(registry, factory, process) {
19
20
  this.#registry = registry
20
21
  this.#factory = factory
21
22
  this.#process = process
22
23
  }
23
24
 
24
- composition (composition) {
25
+ composition(composition) {
25
26
  return this.#create('composition', composition)
26
27
  }
27
28
 
28
- service (path, service) {
29
+ service(path, service) {
29
30
  return this.#create('service', path, service)
30
31
  }
31
32
 
32
- async prepare (root) {
33
+ mono(composition) {
34
+ return this.#create('mono', composition)
35
+ }
36
+
37
+ async prepare(root) {
33
38
  const path = await workspace.create('images', root)
34
39
 
35
- await Promise.all(this.#images.map((image) => image.prepare(path)))
40
+ await Promise.all(this.#all().map((image) => image.prepare(path)))
36
41
 
37
42
  return path
38
43
  }
39
44
 
40
- async build () {
41
- await this.prepare()
45
+ async build() {
46
+ await this.#run(false)
47
+ }
42
48
 
43
- for (const image of this.#images) {
44
- await this.#build(image)
45
- }
49
+ async push() {
50
+ await this.#run(true)
46
51
  }
47
52
 
48
- async push () {
49
- 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
+ }
50
70
 
51
- for (const image of this.#images) await this.#push(image)
71
+ tags() {
72
+ return this.#images.map((image) => image.reference)
52
73
  }
53
74
 
54
75
  /**
55
- * @param {'composition' | 'service'} type
76
+ * @param {'composition' | 'service' | 'mono'} type
56
77
  * @param {...any} args
57
78
  * @returns {toa.deployment.images.Image}
58
79
  */
59
- #create (type, ...args) {
80
+ #create(type, ...args) {
60
81
  const image = this.#factory[type](...args)
61
82
 
62
83
  this.#images.push(image)
@@ -64,58 +85,201 @@ class Registry {
64
85
  return image
65
86
  }
66
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
+
67
142
  /**
68
143
  * @param {toa.deployment.images.Image} image
69
144
  * @param {boolean} [push]
70
145
  * @returns {Promise<void>}
71
146
  */
72
- async #build (image, push = false) {
147
+ async #build(image, push = false) {
73
148
  const args = ['--context=default', 'buildx', 'build']
74
149
 
75
- if (push) {
76
- args.push('--push')
77
- } else {
78
- args.push('--load')
79
- }
150
+ if (push) args.push('--push')
151
+ else args.push('--load')
80
152
 
81
153
  args.push('--tag', image.reference, image.context)
82
154
 
83
- const multiarch = this.#registry.platforms !== null
84
-
85
- if (this.#registry.build?.arguments !== undefined) {
86
- 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]}`)
87
158
  }
88
159
 
89
- if (multiarch) {
90
- const platform = this.#registry.platforms.join(',')
91
- const builder = await this.#createBuilder()
160
+ const platforms = this.#registry.platforms
92
161
 
93
- args.push('--platform', platform)
94
- args.push('--builder', builder)
162
+ if (platforms !== null) args.push('--platform', platforms.join(','))
95
163
 
96
- } else {
97
- args.push('--builder', 'default')
98
- }
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())
169
+
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')
173
+ } else args.push('--builder', 'default')
99
174
 
100
175
  args.push('--progress', 'plain')
101
176
 
102
177
  await this.#process.execute('docker', args)
103
178
  }
104
179
 
105
- async #push (image) {
106
- 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)
194
+ }
195
+
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')
201
+
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
107
213
  }
108
214
 
109
- async #createBuilder () {
110
- const name = `toa-${newid()}`
111
- const create = `buildx create --name ${name} --bootstrap --use`.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()
112
219
 
113
- await this.#process.execute('docker', create)
220
+ return this.#builder
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
+ }
114
243
 
115
- return name
244
+ return BUILDER
116
245
  }
117
246
  }
118
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
274
+ }
275
+
119
276
  const BUILDER = 'toa'
120
277
 
121
- 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