@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/operations",
3
- "version": "0.2.0-dev.2",
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.2",
29
+ "@toa.io/gears": "0.2.0-dev.3+8334154",
30
30
  "execa": "5.1.1"
31
31
  },
32
- "gitHead": "83480176c88640dd2bed2a775ce2d13b95341f19"
32
+ "gitHead": "8334154c1b8a8268ad90adfb15b43a876459014f"
33
33
  }
@@ -0,0 +1,10 @@
1
+ # TODO: bridge specifics
2
+
3
+ FROM node:alpine
4
+
5
+ RUN npm i -g @toa.io/runtime@{{version}}
6
+
7
+ WORKDIR /composition
8
+ ADD . .
9
+
10
+ CMD toa compose *
@@ -0,0 +1,7 @@
1
+ # This file just helps with IDE autocompletion and helm debugging. Actually dynamically replaced by ../chart.js.
2
+ apiVersion: v2
3
+ name: toa-application
4
+ description: Toa Application Chart
5
+ type: application
6
+ version: 0.0.0
7
+ appVersion: "0.0.0"
@@ -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
@@ -1,15 +1,79 @@
1
1
  'use strict'
2
2
 
3
- const chart = (context, dependencies) => {
4
- return {
5
- apiVersion: 'v2',
6
- type: 'application',
7
- name: context.name,
8
- description: context.description,
9
- version: context.version,
10
- appVersion: context.version,
11
- dependencies: dependencies.map((dependency) => dependency.chart)
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.chart = chart
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,7 @@
1
+ 'use strict'
2
+
3
+ const execa = require('execa')
4
+
5
+ const copy = async (source, target) => await execa('cp', ['-r', source, target])
6
+
7
+ exports.copy = copy
@@ -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
- if (path === undefined) {
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
- const entries = await fs.readdir(path)
10
+ const entries = await fs.readdir(path)
14
11
 
15
- if (entries.length > 0) throw new Error('Target directory must be empty')
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
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const { Factory } = require('./factory')
4
+
5
+ exports.Factory = Factory
package/src/index.js CHANGED
@@ -1,7 +1,3 @@
1
1
  'use strict'
2
2
 
3
- const { Images } = require('./images')
4
- const { Deployment } = require('./deployment')
5
-
6
- exports.Images = Images
7
- exports.Deployment = Deployment
3
+ exports.deployment = require('./deployment')
@@ -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
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const values = (context, dependencies) => {
4
- return Object.fromEntries(dependencies.map(({ chart, values }) => [chart.alias || chart.name, values]))
5
- }
6
-
7
- exports.values = values
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
@@ -1,10 +0,0 @@
1
- #TODO: bridge specifics
2
-
3
- FROM node:alpine
4
-
5
- RUN npm i -g @toa.io/runtime@0.1.0-dev.13
6
-
7
- WORKDIR /app
8
- ADD . .
9
-
10
- CMD toa compose .
@@ -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
@@ -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
- })
@@ -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
@@ -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
- })
@@ -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
@@ -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
- })