@toa.io/operations 0.2.0-dev.2 → 0.2.0-dev.3
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/package.json +3 -3
- package/src/deployment/assets/Dockerfile +10 -0
- package/src/deployment/assets/chart/Chart.yaml +7 -0
- package/src/deployment/assets/chart/templates/deployment.yaml +23 -0
- package/src/deployment/assets/chart/templates/service.yaml +16 -0
- package/src/deployment/assets/chart/values.yaml +18 -0
- package/src/deployment/chart.js +74 -10
- package/src/deployment/composition.js +17 -0
- package/src/deployment/compositions.js +60 -0
- package/src/deployment/copy.js +7 -0
- package/src/deployment/deployment.js +76 -0
- package/src/deployment/directory.js +5 -7
- package/src/deployment/factory.js +37 -0
- package/src/deployment/image.js +67 -0
- package/src/deployment/index.js +5 -0
- package/src/index.js +1 -5
- package/test/deployment/compositions.test.js +31 -0
- package/test/deployment/copositions.fixtures.js +42 -0
- package/test/deployment/image.fixtures.js +26 -0
- package/test/deployment/image.test.js +22 -0
- package/src/deployment/dependencies.js +0 -25
- package/src/deployment/values.js +0 -7
- package/src/deployment.js +0 -64
- package/src/images/Dockerfile +0 -10
- package/src/images/image.js +0 -38
- package/src/images.js +0 -26
- package/test/deployment.fixtures.js +0 -34
- package/test/deployment.test.js +0 -88
- package/test/image.fixtures.js +0 -24
- package/test/image.test.js +0 -45
- package/test/images.fixtures.js +0 -29
- package/test/images.test.js +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "0.2.0-dev.
|
|
3
|
+
"version": "0.2.0-dev.3+8334154",
|
|
4
4
|
"description": "Toa Deployment",
|
|
5
5
|
"homepage": "https://toa.io",
|
|
6
6
|
"author": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@toa.io/gears": "0.2.0-dev.
|
|
29
|
+
"@toa.io/gears": "0.2.0-dev.3+8334154",
|
|
30
30
|
"execa": "5.1.1"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "8334154c1b8a8268ad90adfb15b43a876459014f"
|
|
33
33
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{{- range .Values.compositions }}
|
|
2
|
+
apiVersion: apps/v1
|
|
3
|
+
kind: Deployment
|
|
4
|
+
metadata:
|
|
5
|
+
name: composition-{{ required "deployment name is required" .name }}
|
|
6
|
+
spec:
|
|
7
|
+
replicas: {{ .replicas | default 2 }}
|
|
8
|
+
selector:
|
|
9
|
+
matchLabels:
|
|
10
|
+
toa.io/composition: {{ .name }}
|
|
11
|
+
template:
|
|
12
|
+
metadata:
|
|
13
|
+
labels:
|
|
14
|
+
toa.io/composition: {{ .name }}
|
|
15
|
+
{{- range .components }}
|
|
16
|
+
{{ . }}: "1"
|
|
17
|
+
{{- end }}
|
|
18
|
+
spec:
|
|
19
|
+
containers:
|
|
20
|
+
- name: {{ .name }}
|
|
21
|
+
image: {{ .image }}
|
|
22
|
+
---
|
|
23
|
+
{{- end }}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{- range .Values.components }}
|
|
2
|
+
apiVersion: v1
|
|
3
|
+
kind: Service
|
|
4
|
+
metadata:
|
|
5
|
+
name: {{ . }}
|
|
6
|
+
spec:
|
|
7
|
+
type: ClusterIP
|
|
8
|
+
selector:
|
|
9
|
+
{{ . }}: "1"
|
|
10
|
+
ports:
|
|
11
|
+
- name: http-binding
|
|
12
|
+
protocol: TCP
|
|
13
|
+
port: 3000
|
|
14
|
+
targetPort: 3000
|
|
15
|
+
---
|
|
16
|
+
{{- end }}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# This file just helps with IDE autocompletion and helm debugging. Actually dynamically replaced by ../values.js.
|
|
2
|
+
compositions:
|
|
3
|
+
- name: todos
|
|
4
|
+
image: localhost:5000/todos:0.0.0
|
|
5
|
+
replicas: 2
|
|
6
|
+
components:
|
|
7
|
+
- todos-tasks
|
|
8
|
+
- todos-stats
|
|
9
|
+
- name: messages
|
|
10
|
+
image: localhost:5000/messages:0.0.0
|
|
11
|
+
replicas: 2
|
|
12
|
+
components:
|
|
13
|
+
- users-users
|
|
14
|
+
# TODO: create services only if sync binding is being used by a component
|
|
15
|
+
components:
|
|
16
|
+
- todos-tasks
|
|
17
|
+
- todos-stats
|
|
18
|
+
- users-users
|
package/src/deployment/chart.js
CHANGED
|
@@ -1,15 +1,79 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
class Chart {
|
|
4
|
+
name
|
|
5
|
+
|
|
6
|
+
#context
|
|
7
|
+
#compositions
|
|
8
|
+
#dependencies
|
|
9
|
+
|
|
10
|
+
constructor (context, compositions) {
|
|
11
|
+
this.name = context.name
|
|
12
|
+
|
|
13
|
+
this.#context = context
|
|
14
|
+
this.#compositions = compositions
|
|
15
|
+
this.#dependencies = Chart.dependencies(context)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get declaration () {
|
|
19
|
+
const { name, description, version } = this.#context
|
|
20
|
+
const dependencies = this.#dependencies.map((dependency) => dependency.chart)
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
apiVersion: 'v2',
|
|
24
|
+
type: 'application',
|
|
25
|
+
name,
|
|
26
|
+
description,
|
|
27
|
+
version,
|
|
28
|
+
appVersion: version,
|
|
29
|
+
dependencies
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get values () {
|
|
34
|
+
const result = {}
|
|
35
|
+
|
|
36
|
+
result.compositions = this.#deployments()
|
|
37
|
+
result.components = this.#context.components.map((component) => component.domain + '-' + component.name)
|
|
38
|
+
|
|
39
|
+
for (const { chart, values } of this.#dependencies) result[chart.alias || chart.name] = values
|
|
40
|
+
|
|
41
|
+
return result
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#deployments () {
|
|
45
|
+
return Array.from(this.#compositions).map((composition) => {
|
|
46
|
+
const { name, components, replicas, image: { tag: image } } = composition
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
components: components.map((component) => component.locator.label),
|
|
51
|
+
replicas,
|
|
52
|
+
image
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static dependencies (context) {
|
|
58
|
+
const map = (map) => {
|
|
59
|
+
const list = []
|
|
60
|
+
|
|
61
|
+
for (const [key, values] of Object.entries(map)) {
|
|
62
|
+
const dependency = require(key)
|
|
63
|
+
|
|
64
|
+
if (dependency.deployments !== undefined) list.push(...dependency.deployments(values))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return list
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const dependencies = []
|
|
71
|
+
|
|
72
|
+
if (context.connectors !== undefined) dependencies.push(...map(context.connectors))
|
|
73
|
+
if (context.extensions !== undefined) dependencies.push(...map(context.extensions))
|
|
74
|
+
|
|
75
|
+
return dependencies
|
|
12
76
|
}
|
|
13
77
|
}
|
|
14
78
|
|
|
15
|
-
exports.
|
|
79
|
+
exports.Chart = Chart
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class Composition {
|
|
4
|
+
name
|
|
5
|
+
components
|
|
6
|
+
replicas
|
|
7
|
+
image
|
|
8
|
+
|
|
9
|
+
constructor (composition, image) {
|
|
10
|
+
this.name = composition.name
|
|
11
|
+
this.components = composition.components
|
|
12
|
+
this.replicas = composition.replicas
|
|
13
|
+
this.image = image
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exports.Composition = Composition
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class Compositions {
|
|
4
|
+
#compositions
|
|
5
|
+
|
|
6
|
+
constructor (context, instantiate) {
|
|
7
|
+
if (context.compositions === undefined) context.compositions = []
|
|
8
|
+
|
|
9
|
+
Compositions.#resolve(context)
|
|
10
|
+
Compositions.#complete(context)
|
|
11
|
+
|
|
12
|
+
this.#compositions = context.compositions.map(instantiate)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[Symbol.iterator] = () => this.#compositions.values()
|
|
16
|
+
|
|
17
|
+
static #resolve (context) {
|
|
18
|
+
const map = {}
|
|
19
|
+
|
|
20
|
+
for (const component of context.components) {
|
|
21
|
+
const id = component.locator.id
|
|
22
|
+
|
|
23
|
+
map[id] = component
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const composition of context.compositions) {
|
|
27
|
+
composition.components = composition.components.map((id) => {
|
|
28
|
+
const component = map[id]
|
|
29
|
+
|
|
30
|
+
if (component === undefined) {
|
|
31
|
+
throw new Error(`Unknown component '${id}' within composition '${composition.name}'`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return component
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static #complete (context) {
|
|
40
|
+
const composed = new Set(context.compositions.map((composition) =>
|
|
41
|
+
composition.components.map((component) => component.locator.id)
|
|
42
|
+
).flat())
|
|
43
|
+
|
|
44
|
+
const names = new Set(context.compositions.map((composition) => composition.name))
|
|
45
|
+
|
|
46
|
+
for (const component of context.components) {
|
|
47
|
+
const { id, label } = component.locator
|
|
48
|
+
|
|
49
|
+
if (composed.has(id)) continue
|
|
50
|
+
if (names.has(label)) throw new Error(`Duplicate composition name '${label}'`)
|
|
51
|
+
|
|
52
|
+
context.compositions.push({
|
|
53
|
+
name: label,
|
|
54
|
+
components: [component]
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exports.Compositions = Compositions
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { writeFile: write, rm: remove } = require('node:fs/promises')
|
|
4
|
+
const execa = require('execa')
|
|
5
|
+
const { join } = require('node:path')
|
|
6
|
+
const { yaml } = require('@toa.io/gears')
|
|
7
|
+
|
|
8
|
+
const { directory } = require('./directory')
|
|
9
|
+
const { copy } = require('./copy')
|
|
10
|
+
|
|
11
|
+
class Deployment {
|
|
12
|
+
#chart
|
|
13
|
+
#images
|
|
14
|
+
|
|
15
|
+
constructor (chart, images) {
|
|
16
|
+
this.#chart = chart
|
|
17
|
+
this.#images = images
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async export (path) {
|
|
21
|
+
if (path === undefined) path = await directory.temp('deployment')
|
|
22
|
+
else await directory(path)
|
|
23
|
+
|
|
24
|
+
await this.#dump(path)
|
|
25
|
+
|
|
26
|
+
return path
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async install (options = {}) {
|
|
30
|
+
await this.#push()
|
|
31
|
+
return await this.#upgrade(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async #dump (path) {
|
|
35
|
+
const chart = yaml.dump(this.#chart.declaration)
|
|
36
|
+
const values = yaml.dump(this.#chart.values)
|
|
37
|
+
|
|
38
|
+
await copy(join(__dirname, 'assets/chart/templates'), join(path, 'templates'))
|
|
39
|
+
|
|
40
|
+
await Promise.all([
|
|
41
|
+
write(join(path, 'Chart.yaml'), chart),
|
|
42
|
+
write(join(path, 'values.yaml'), values)
|
|
43
|
+
])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async #push () {
|
|
47
|
+
for (const image of this.#images) {
|
|
48
|
+
await image.build()
|
|
49
|
+
await image.push()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async #upgrade (options) {
|
|
54
|
+
const path = await this.export()
|
|
55
|
+
const args = []
|
|
56
|
+
|
|
57
|
+
if (options.wait === true) args.push('--wait')
|
|
58
|
+
if (options.dry === true) args.push('--dry-run')
|
|
59
|
+
|
|
60
|
+
const update = execa('helm', ['dependency', 'update', path])
|
|
61
|
+
|
|
62
|
+
update.stdout.pipe(process.stdout)
|
|
63
|
+
await update
|
|
64
|
+
|
|
65
|
+
const upgrade = execa('helm', ['upgrade', this.#chart.name, '-i', ...args, path])
|
|
66
|
+
|
|
67
|
+
upgrade.stdout.pipe(process.stdout)
|
|
68
|
+
const output = await upgrade
|
|
69
|
+
|
|
70
|
+
await remove(path, { recursive: true })
|
|
71
|
+
|
|
72
|
+
return output
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.Deployment = Deployment
|
|
@@ -5,17 +5,15 @@ const { join } = require('node:path')
|
|
|
5
5
|
const { tmpdir } = require('node:os')
|
|
6
6
|
|
|
7
7
|
const directory = async (path) => {
|
|
8
|
-
|
|
9
|
-
path = await fs.mkdtemp(join(tmpdir(), 'toa-deployment-'))
|
|
10
|
-
} else {
|
|
11
|
-
await fs.mkdir(path, { recursive: true })
|
|
8
|
+
await fs.mkdir(path, { recursive: true })
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
const entries = await fs.readdir(path)
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
}
|
|
12
|
+
if (entries.length > 0) throw new Error('Target directory must be empty')
|
|
17
13
|
|
|
18
14
|
return path
|
|
19
15
|
}
|
|
20
16
|
|
|
17
|
+
directory.temp = async (type) => await fs.mkdtemp(join(tmpdir(), `toa-${type}-`))
|
|
18
|
+
|
|
21
19
|
exports.directory = directory
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Composition } = require('./composition')
|
|
4
|
+
const { Compositions } = require('./compositions')
|
|
5
|
+
const { Image } = require('./image')
|
|
6
|
+
const { Deployment } = require('./deployment')
|
|
7
|
+
const { Chart } = require('./chart')
|
|
8
|
+
|
|
9
|
+
class Factory {
|
|
10
|
+
#context
|
|
11
|
+
|
|
12
|
+
constructor (context) {
|
|
13
|
+
this.#context = context
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
deployment () {
|
|
17
|
+
const compositions = this.#compositions()
|
|
18
|
+
const images = Array.from(compositions).map((composition) => composition.image)
|
|
19
|
+
const chart = new Chart(this.#context, compositions)
|
|
20
|
+
|
|
21
|
+
return new Deployment(chart, images)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#compositions () {
|
|
25
|
+
return new Compositions(this.#context, (composition) => this.#composition(composition))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#composition (composition) {
|
|
29
|
+
const image = this.#image(composition)
|
|
30
|
+
|
|
31
|
+
return new Composition(composition, image)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#image = (composition) => new Image(composition, this.#context)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
exports.Factory = Factory
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { join } = require('node:path')
|
|
4
|
+
const { readFile: read, writeFile: write, rm: remove } = require('node:fs/promises')
|
|
5
|
+
const execa = require('execa')
|
|
6
|
+
|
|
7
|
+
const { hash } = require('@toa.io/gears')
|
|
8
|
+
const { directory } = require('./directory')
|
|
9
|
+
const { copy } = require('./copy')
|
|
10
|
+
|
|
11
|
+
class Image {
|
|
12
|
+
tag
|
|
13
|
+
|
|
14
|
+
#composition
|
|
15
|
+
#registry
|
|
16
|
+
#runtime
|
|
17
|
+
|
|
18
|
+
constructor (composition, context) {
|
|
19
|
+
this.#composition = composition
|
|
20
|
+
this.#registry = context.registry
|
|
21
|
+
this.#runtime = context.runtime
|
|
22
|
+
|
|
23
|
+
this.tag = context.registry + '/' + composition.name + ':' + Image.#tag(composition)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async build () {
|
|
27
|
+
const path = await this.#context()
|
|
28
|
+
const build = execa('docker', ['build', path, '-t', this.tag])
|
|
29
|
+
|
|
30
|
+
build.stdout.pipe(process.stdout)
|
|
31
|
+
|
|
32
|
+
await build
|
|
33
|
+
|
|
34
|
+
await remove(path, { recursive: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async push () {
|
|
38
|
+
const push = execa('docker', ['push', this.tag])
|
|
39
|
+
|
|
40
|
+
push.stdout.pipe(process.stdout)
|
|
41
|
+
|
|
42
|
+
await push
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #context () {
|
|
46
|
+
const path = await directory.temp('composition')
|
|
47
|
+
const dockerfile = (await read(DOCKERFILE, 'utf-8')).replace('{{version}}', this.#runtime)
|
|
48
|
+
|
|
49
|
+
for (const component of this.#composition.components) {
|
|
50
|
+
await copy(component.path, join(path, component.locator.id))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await write(join(path, 'Dockerfile'), dockerfile)
|
|
54
|
+
|
|
55
|
+
return path
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static #tag (composition) {
|
|
59
|
+
const components = composition.components.map((component) => component.locator.id).join(';')
|
|
60
|
+
|
|
61
|
+
return hash(components)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DOCKERFILE = join(__dirname, 'assets/Dockerfile')
|
|
66
|
+
|
|
67
|
+
exports.Image = Image
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fixtures = require('./copositions.fixtures')
|
|
4
|
+
const { Compositions } = require('../../src/deployment/compositions')
|
|
5
|
+
|
|
6
|
+
const clone = require('clone-deep')
|
|
7
|
+
|
|
8
|
+
let context, compositions
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
context = clone(fixtures.context)
|
|
12
|
+
compositions = new Compositions(context, fixtures.instantiate)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should instantiate', () => {
|
|
16
|
+
expect(fixtures.instantiate).toHaveBeenCalledTimes(context.compositions.length)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should be iterable over instances', () => {
|
|
20
|
+
expect(Symbol.iterator in compositions).toEqual(true)
|
|
21
|
+
|
|
22
|
+
for (const composition of compositions) expect(fixtures.instantiate).toHaveReturnedWith(composition)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should complete compositions', () => {
|
|
26
|
+
expect(context.compositions.length).toBeGreaterThan(fixtures.context.compositions.length)
|
|
27
|
+
|
|
28
|
+
const used = new Set(context.compositions.map((composition) => composition.components).flat())
|
|
29
|
+
|
|
30
|
+
expect(used.size).toEqual(context.components.length)
|
|
31
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { repeat, random } = require('@toa.io/gears')
|
|
4
|
+
const { generate } = require('randomstring')
|
|
5
|
+
|
|
6
|
+
const components = repeat(() => ({
|
|
7
|
+
domain: generate(),
|
|
8
|
+
name: generate(),
|
|
9
|
+
locator: {
|
|
10
|
+
id: generate(),
|
|
11
|
+
label: generate()
|
|
12
|
+
},
|
|
13
|
+
version: random(10) + '.' + random(20) + '.' + random(30)
|
|
14
|
+
}), random(10) + 5)
|
|
15
|
+
|
|
16
|
+
const context = {
|
|
17
|
+
registry: generate(),
|
|
18
|
+
components,
|
|
19
|
+
compositions: []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let unused = components.length
|
|
23
|
+
|
|
24
|
+
while (1) {
|
|
25
|
+
const use = random(3) + 1
|
|
26
|
+
const index = components.length - unused
|
|
27
|
+
|
|
28
|
+
if (use >= unused) break
|
|
29
|
+
|
|
30
|
+
context.compositions.push({
|
|
31
|
+
name: generate(),
|
|
32
|
+
components: components.slice(index, index + use)
|
|
33
|
+
.map((component) => component.locator.id)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
unused -= use
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const instantiate = jest.fn(() => generate())
|
|
40
|
+
|
|
41
|
+
exports.context = context
|
|
42
|
+
exports.instantiate = instantiate
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { newid, random, repeat } = require('@toa.io/gears')
|
|
4
|
+
const { join } = require('node:path')
|
|
5
|
+
const { generate } = require('randomstring')
|
|
6
|
+
|
|
7
|
+
const mock = {
|
|
8
|
+
execa: jest.fn(() => ({ stdout: { pipe: jest.fn() } }))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const composition = {
|
|
12
|
+
name: generate(),
|
|
13
|
+
components: repeat(() => ({ locator: { id: newid() } }), random(5) + 5)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const context = {
|
|
17
|
+
registry: `registry-${newid()}:${random(999) + 5000}`,
|
|
18
|
+
runtime: `${random(9)}.${random(9)}.${random(20)}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DOCKERFILE = join(__dirname, '../src/images/Dockerfile')
|
|
22
|
+
|
|
23
|
+
exports.mock = mock
|
|
24
|
+
exports.composition = composition
|
|
25
|
+
exports.context = context
|
|
26
|
+
exports.DOCKERFILE = DOCKERFILE
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fixtures = require('./image.fixtures')
|
|
4
|
+
const mock = fixtures.mock
|
|
5
|
+
|
|
6
|
+
jest.mock('execa', () => mock.execa)
|
|
7
|
+
|
|
8
|
+
const { Image } = require('../../src/deployment/image')
|
|
9
|
+
const { hash } = require('@toa.io/gears')
|
|
10
|
+
|
|
11
|
+
let image
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
image = new Image(fixtures.composition, fixtures.context)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should provide tag', () => {
|
|
18
|
+
const tag = fixtures.context.registry + '/' + fixtures.composition.name + ':' +
|
|
19
|
+
hash(fixtures.composition.components.map((component) => component.locator.id).join(';'))
|
|
20
|
+
|
|
21
|
+
expect(image.tag).toEqual(tag)
|
|
22
|
+
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const dependencies = (context) => {
|
|
4
|
-
const dependencies = map(context.connectors)
|
|
5
|
-
|
|
6
|
-
if (context.extensions !== undefined) dependencies.push(...map(context.extensions))
|
|
7
|
-
|
|
8
|
-
return dependencies
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const map = (map) => {
|
|
12
|
-
const list = []
|
|
13
|
-
|
|
14
|
-
for (const [key, values] of Object.entries(map)) {
|
|
15
|
-
const dependency = require(key)
|
|
16
|
-
|
|
17
|
-
if (dependency.deployments !== undefined) {
|
|
18
|
-
list.push(...dependency.deployments(values))
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return list
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
exports.dependencies = dependencies
|
package/src/deployment/values.js
DELETED
package/src/deployment.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs/promises')
|
|
4
|
-
const execa = require('execa')
|
|
5
|
-
const { join } = require('node:path')
|
|
6
|
-
const { yaml } = require('@toa.io/gears')
|
|
7
|
-
|
|
8
|
-
const { dependencies } = require('./deployment/dependencies')
|
|
9
|
-
const { directory } = require('./deployment/directory')
|
|
10
|
-
const { chart } = require('./deployment/chart')
|
|
11
|
-
const { values } = require('./deployment/values')
|
|
12
|
-
|
|
13
|
-
class Deployment {
|
|
14
|
-
#context
|
|
15
|
-
#chart
|
|
16
|
-
#values
|
|
17
|
-
|
|
18
|
-
constructor (context) {
|
|
19
|
-
const deps = dependencies(context)
|
|
20
|
-
|
|
21
|
-
this.#context = context
|
|
22
|
-
this.#chart = chart(context, deps)
|
|
23
|
-
this.#values = values(context, deps)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async export (path) {
|
|
27
|
-
path = await directory(path)
|
|
28
|
-
|
|
29
|
-
await this.#dump(path)
|
|
30
|
-
|
|
31
|
-
return path
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async install (wait) {
|
|
35
|
-
const path = await this.export()
|
|
36
|
-
const args = []
|
|
37
|
-
|
|
38
|
-
if (wait === true) args.push('--wait')
|
|
39
|
-
|
|
40
|
-
const update = execa('helm', ['dependency', 'update', path])
|
|
41
|
-
|
|
42
|
-
update.stdout.pipe(process.stdout)
|
|
43
|
-
await update
|
|
44
|
-
|
|
45
|
-
const upgrade = execa('helm', ['upgrade', this.#context.name, '-i', ...args, path])
|
|
46
|
-
|
|
47
|
-
upgrade.stdout.pipe(process.stdout)
|
|
48
|
-
await upgrade
|
|
49
|
-
|
|
50
|
-
await fs.rm(path, { recursive: true })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async #dump (path) {
|
|
54
|
-
const chart = yaml.dump(this.#chart)
|
|
55
|
-
const values = yaml.dump(this.#values)
|
|
56
|
-
|
|
57
|
-
await Promise.all([
|
|
58
|
-
fs.writeFile(join(path, 'Chart.yaml'), chart),
|
|
59
|
-
fs.writeFile(join(path, 'values.yaml'), values)
|
|
60
|
-
])
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
exports.Deployment = Deployment
|
package/src/images/Dockerfile
DELETED
package/src/images/image.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { join } = require('node:path')
|
|
4
|
-
const execa = require('execa')
|
|
5
|
-
|
|
6
|
-
class Image {
|
|
7
|
-
#manifest
|
|
8
|
-
#registry
|
|
9
|
-
#tag
|
|
10
|
-
|
|
11
|
-
constructor (manifest, registry) {
|
|
12
|
-
this.#manifest = manifest
|
|
13
|
-
this.#registry = registry
|
|
14
|
-
|
|
15
|
-
const { domain, name, version } = this.#manifest
|
|
16
|
-
this.#tag = this.#registry + '/' + domain + '-' + name + ':' + version
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async build () {
|
|
20
|
-
const build = execa('docker', ['build', this.#manifest.path, '-f', DOCKERFILE, '-t', this.#tag])
|
|
21
|
-
|
|
22
|
-
build.stdout.pipe(process.stdout)
|
|
23
|
-
|
|
24
|
-
await build
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async push () {
|
|
28
|
-
const push = execa('docker', ['push', this.#tag])
|
|
29
|
-
|
|
30
|
-
push.stdout.pipe(process.stdout)
|
|
31
|
-
|
|
32
|
-
await push
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const DOCKERFILE = join(__dirname, 'Dockerfile')
|
|
37
|
-
|
|
38
|
-
exports.Image = Image
|
package/src/images.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Image } = require('./images/image')
|
|
4
|
-
|
|
5
|
-
class Images {
|
|
6
|
-
#images
|
|
7
|
-
|
|
8
|
-
constructor (context) {
|
|
9
|
-
this.#images = context.manifests.map((manifest) => new Image(manifest, context.registry))
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async push () {
|
|
13
|
-
await this.#build()
|
|
14
|
-
await this.#push()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async #build () {
|
|
18
|
-
await Promise.all(this.#images.map((image) => image.build()))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async #push () {
|
|
22
|
-
await Promise.all(this.#images.map((image) => image.push()))
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
exports.Images = Images
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { generate } = require('randomstring')
|
|
4
|
-
|
|
5
|
-
const context = {
|
|
6
|
-
name: generate(),
|
|
7
|
-
description: generate(),
|
|
8
|
-
version: '0.0.1',
|
|
9
|
-
runtime: '0.1.0',
|
|
10
|
-
packages: './path/to/' + generate()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const mock = {
|
|
14
|
-
dependencies: {
|
|
15
|
-
dependencies: jest.fn(() => [generate(), generate()])
|
|
16
|
-
},
|
|
17
|
-
directory: {
|
|
18
|
-
directory: jest.fn(() => generate())
|
|
19
|
-
},
|
|
20
|
-
chart: {
|
|
21
|
-
chart: jest.fn(() => ({ [generate()]: generate() }))
|
|
22
|
-
},
|
|
23
|
-
values: {
|
|
24
|
-
values: jest.fn(() => ({ [generate()]: generate() }))
|
|
25
|
-
},
|
|
26
|
-
fs: {
|
|
27
|
-
writeFile: jest.fn(),
|
|
28
|
-
rm: jest.fn()
|
|
29
|
-
},
|
|
30
|
-
execa: jest.fn(() => ({ stdout: { pipe: jest.fn() } }))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
exports.context = context
|
|
34
|
-
exports.mock = mock
|
package/test/deployment.test.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fixtures = require('./deployment.fixtures')
|
|
4
|
-
const mock = fixtures.mock
|
|
5
|
-
|
|
6
|
-
jest.mock('node:fs/promises', () => mock.fs)
|
|
7
|
-
jest.mock('execa', () => mock.execa)
|
|
8
|
-
jest.mock('../src/deployment/dependencies', () => mock.dependencies)
|
|
9
|
-
jest.mock('../src/deployment/directory', () => mock.directory)
|
|
10
|
-
jest.mock('../src/deployment/chart', () => mock.chart)
|
|
11
|
-
jest.mock('../src/deployment/values', () => mock.values)
|
|
12
|
-
|
|
13
|
-
const { Deployment } = require('../src')
|
|
14
|
-
const { join } = require('node:path')
|
|
15
|
-
const { generate } = require('randomstring')
|
|
16
|
-
const { yaml } = require('@toa.io/gears')
|
|
17
|
-
|
|
18
|
-
let deployment
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
deployment = new Deployment(fixtures.context)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('export', () => {
|
|
25
|
-
const test = async (arg) => {
|
|
26
|
-
const result = await deployment.export(arg)
|
|
27
|
-
|
|
28
|
-
if (arg !== undefined) expect(arg).toBe(mock.directory.directory.mock.calls[0][0])
|
|
29
|
-
|
|
30
|
-
const path = mock.directory.directory.mock.results[0].value
|
|
31
|
-
const chart = yaml.dump(mock.chart.chart.mock.results[0].value)
|
|
32
|
-
const values = yaml.dump(mock.values.values.mock.results[0].value)
|
|
33
|
-
|
|
34
|
-
expect(mock.fs.writeFile).toHaveBeenNthCalledWith(1, join(path, 'Chart.yaml'), chart)
|
|
35
|
-
expect(mock.fs.writeFile).toHaveBeenNthCalledWith(2, join(path, 'values.yaml'), values)
|
|
36
|
-
expect(result).toBe(path)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
it('should export chart', async () => {
|
|
40
|
-
await test()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('should export chart to given path', async () => {
|
|
44
|
-
await test(generate())
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('install', () => {
|
|
49
|
-
let path
|
|
50
|
-
|
|
51
|
-
beforeEach(async () => {
|
|
52
|
-
await deployment.install()
|
|
53
|
-
|
|
54
|
-
path = mock.directory.directory.mock.results[0].value
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('should update', () => {
|
|
58
|
-
expect(mock.execa).toHaveBeenNthCalledWith(1, 'helm', ['dependency', 'update', path])
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('should upgrade', () => {
|
|
62
|
-
expect(mock.execa).toHaveBeenNthCalledWith(2,
|
|
63
|
-
'helm', ['upgrade', fixtures.context.name, '-i', path])
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should wait on upgrade', async () => {
|
|
67
|
-
jest.clearAllMocks()
|
|
68
|
-
|
|
69
|
-
await deployment.install(true)
|
|
70
|
-
|
|
71
|
-
path = mock.directory.directory.mock.results[0].value
|
|
72
|
-
|
|
73
|
-
expect(mock.execa).toHaveBeenNthCalledWith(2,
|
|
74
|
-
'helm', ['upgrade', fixtures.context.name, '-i', '--wait', path])
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('should pipe stdout', () => {
|
|
78
|
-
const update = mock.execa.mock.results[0].value
|
|
79
|
-
const upgrade = mock.execa.mock.results[1].value
|
|
80
|
-
|
|
81
|
-
expect(update.stdout.pipe).toHaveBeenCalledWith(process.stdout)
|
|
82
|
-
expect(upgrade.stdout.pipe).toHaveBeenCalledWith(process.stdout)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should clear', () => {
|
|
86
|
-
expect(mock.fs.rm).toHaveBeenCalledWith(path, { recursive: true })
|
|
87
|
-
})
|
|
88
|
-
})
|
package/test/image.fixtures.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { newid, random } = require('@toa.io/gears')
|
|
4
|
-
const { join } = require('node:path')
|
|
5
|
-
|
|
6
|
-
const mock = {
|
|
7
|
-
execa: jest.fn(() => ({ stdout: { pipe: jest.fn() } }))
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const manifest = {
|
|
11
|
-
domain: 'domain' + newid(),
|
|
12
|
-
name: 'component' + newid(),
|
|
13
|
-
version: '0.0.' + random(9),
|
|
14
|
-
path: newid()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const registry = `registry-${newid()}:${random(999) + 5000}`
|
|
18
|
-
|
|
19
|
-
const DOCKERFILE = join(__dirname, '../src/images/Dockerfile')
|
|
20
|
-
|
|
21
|
-
exports.mock = mock
|
|
22
|
-
exports.manifest = manifest
|
|
23
|
-
exports.registry = registry
|
|
24
|
-
exports.DOCKERFILE = DOCKERFILE
|
package/test/image.test.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fixtures = require('./image.fixtures')
|
|
4
|
-
const mock = fixtures.mock
|
|
5
|
-
|
|
6
|
-
jest.mock('execa', () => mock.execa)
|
|
7
|
-
|
|
8
|
-
const { Image } = require('../src/images/image')
|
|
9
|
-
|
|
10
|
-
let image
|
|
11
|
-
let tag
|
|
12
|
-
|
|
13
|
-
beforeAll(() => {
|
|
14
|
-
const { domain, name, version } = fixtures.manifest
|
|
15
|
-
|
|
16
|
-
tag = `${fixtures.registry}/${domain}-${name}:${version}`
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
image = new Image(fixtures.manifest, fixtures.registry)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('should build', async () => {
|
|
24
|
-
await image.build()
|
|
25
|
-
|
|
26
|
-
expect(mock.execa).toHaveBeenCalledWith('docker',
|
|
27
|
-
['build', fixtures.manifest.path, '-f', fixtures.DOCKERFILE, '-t', tag])
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should push', async () => {
|
|
31
|
-
await image.push()
|
|
32
|
-
|
|
33
|
-
expect(mock.execa).toHaveBeenCalledWith('docker', ['push', tag])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should pipe stdout', async () => {
|
|
37
|
-
await image.build()
|
|
38
|
-
await image.push()
|
|
39
|
-
|
|
40
|
-
const build = mock.execa.mock.results[0].value
|
|
41
|
-
const push = mock.execa.mock.results[1].value
|
|
42
|
-
|
|
43
|
-
expect(build.stdout.pipe).toHaveBeenCalledWith(process.stdout)
|
|
44
|
-
expect(push.stdout.pipe).toHaveBeenCalledWith(process.stdout)
|
|
45
|
-
})
|
package/test/images.fixtures.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { random, repeat, newid } = require('@toa.io/gears')
|
|
4
|
-
|
|
5
|
-
const mock = {
|
|
6
|
-
image: {
|
|
7
|
-
Image: jest.fn(() => {
|
|
8
|
-
return {
|
|
9
|
-
build: jest.fn(),
|
|
10
|
-
push: jest.fn()
|
|
11
|
-
}
|
|
12
|
-
})
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const context = {
|
|
17
|
-
name: 'context-' + newid(),
|
|
18
|
-
registry: `registry-${newid()}:${random(999) + 5000}`,
|
|
19
|
-
manifests: repeat(
|
|
20
|
-
() => ({
|
|
21
|
-
domain: 'domain-' + newid(),
|
|
22
|
-
name: 'component-' + newid(),
|
|
23
|
-
version: '0.0.0'
|
|
24
|
-
}),
|
|
25
|
-
random(9) + 1)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
exports.mock = mock
|
|
29
|
-
exports.context = context
|
package/test/images.test.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fixtures = require('./images.fixtures')
|
|
4
|
-
const mock = fixtures.mock
|
|
5
|
-
|
|
6
|
-
jest.mock('../src/images/image', () => mock.image)
|
|
7
|
-
|
|
8
|
-
const { Images } = require('../src')
|
|
9
|
-
|
|
10
|
-
let images
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
images = new Images(fixtures.context)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('should create Image instances', () => {
|
|
17
|
-
expect(mock.image.Image).toHaveBeenCalledTimes(fixtures.context.manifests.length)
|
|
18
|
-
|
|
19
|
-
for (let i = 0; i < fixtures.context.manifests.length; i++) {
|
|
20
|
-
expect(mock.image.Image).toHaveBeenCalledWith(fixtures.context.manifests[i], fixtures.context.registry)
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('push', () => {
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
await images.push()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should push images', () => {
|
|
30
|
-
mock.image.Image.mock.results.forEach(({ value: instance }) => {
|
|
31
|
-
expect(instance.build).toHaveBeenCalled()
|
|
32
|
-
expect(instance.push).toHaveBeenCalled()
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
})
|