@toa.io/operations 0.2.0-dev.2 → 0.2.1-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.
Files changed (62) hide show
  1. package/package.json +5 -3
  2. package/src/deployment/.deployment/.describe/components.js +19 -0
  3. package/src/deployment/.deployment/.describe/dependencies.js +18 -0
  4. package/src/deployment/.deployment/.describe/index.js +9 -0
  5. package/src/deployment/.deployment/.describe/variables.js +27 -0
  6. package/src/deployment/.deployment/declare.js +29 -0
  7. package/src/deployment/.deployment/describe.js +28 -0
  8. package/src/deployment/.deployment/index.js +9 -0
  9. package/src/deployment/.deployment/merge.js +42 -0
  10. package/src/deployment/chart/Chart.yaml +7 -0
  11. package/src/deployment/chart/templates/components.yaml +16 -0
  12. package/src/deployment/chart/templates/compositions.yaml +38 -0
  13. package/src/deployment/chart/templates/proxies.yaml +10 -0
  14. package/src/deployment/chart/templates/services.yaml +63 -0
  15. package/src/deployment/chart/templates/variables.yaml +12 -0
  16. package/src/deployment/chart/values.yaml +47 -0
  17. package/src/deployment/composition.js +30 -0
  18. package/src/deployment/deployment.js +82 -0
  19. package/src/deployment/factory.js +100 -0
  20. package/src/deployment/images/composition.Dockerfile +14 -0
  21. package/src/deployment/images/composition.js +50 -0
  22. package/src/deployment/images/factory.js +39 -0
  23. package/src/deployment/images/image.js +91 -0
  24. package/src/deployment/images/index.js +7 -0
  25. package/src/deployment/images/registry.js +102 -0
  26. package/src/deployment/images/service.Dockerfile +12 -0
  27. package/src/deployment/images/service.js +70 -0
  28. package/src/deployment/index.js +5 -0
  29. package/src/deployment/operator.js +85 -0
  30. package/src/deployment/service.js +25 -0
  31. package/src/index.js +1 -5
  32. package/src/process.js +25 -0
  33. package/test/deployment/deployment.fixtures.js +18 -0
  34. package/test/deployment/deployment.test.js +41 -0
  35. package/test/deployment/images/image.fixtures.js +33 -0
  36. package/test/deployment/images/image.test.js +25 -0
  37. package/types/deployment/composition.d.ts +13 -0
  38. package/types/deployment/dependency.d.ts +77 -0
  39. package/types/deployment/deployment.d.ts +67 -0
  40. package/types/deployment/factory.d.ts +12 -0
  41. package/types/deployment/images/factory.d.ts +17 -0
  42. package/types/deployment/images/image.d.ts +14 -0
  43. package/types/deployment/images/registry.d.ts +20 -0
  44. package/types/deployment/index.d.ts +1 -0
  45. package/types/deployment/operator.d.ts +21 -0
  46. package/types/deployment/service.d.ts +20 -0
  47. package/types/process.d.ts +13 -0
  48. package/LICENSE +0 -22
  49. package/src/deployment/chart.js +0 -15
  50. package/src/deployment/dependencies.js +0 -25
  51. package/src/deployment/directory.js +0 -21
  52. package/src/deployment/values.js +0 -7
  53. package/src/deployment.js +0 -64
  54. package/src/images/Dockerfile +0 -10
  55. package/src/images/image.js +0 -38
  56. package/src/images.js +0 -26
  57. package/test/deployment.fixtures.js +0 -34
  58. package/test/deployment.test.js +0 -88
  59. package/test/image.fixtures.js +0 -24
  60. package/test/image.test.js +0 -45
  61. package/test/images.fixtures.js +0 -29
  62. package/test/images.test.js +0 -35
@@ -0,0 +1,39 @@
1
+ 'use strict'
2
+
3
+ const { Composition } = require('./composition')
4
+ const { Service } = require('./service')
5
+
6
+ /**
7
+ * @implements {toa.deployment.images.Factory}
8
+ */
9
+ class Factory {
10
+ /** @type {string} */
11
+ #scope
12
+ /** @type {toa.norm.context.Runtime} */
13
+ #runtime
14
+
15
+ /**
16
+ * @param scope {string}
17
+ * @param runtime {toa.norm.context.Runtime}
18
+ */
19
+ constructor (scope, runtime) {
20
+ this.#scope = scope
21
+ this.#runtime = runtime
22
+ }
23
+
24
+ /**
25
+ * @returns {Composition}
26
+ */
27
+ composition (composition) {
28
+ return new Composition(this.#scope, this.#runtime, composition)
29
+ }
30
+
31
+ /**
32
+ * @returns {Service}
33
+ */
34
+ service (path, service) {
35
+ return new Service(this.#scope, this.#runtime, path, service)
36
+ }
37
+ }
38
+
39
+ exports.Factory = Factory
@@ -0,0 +1,91 @@
1
+ 'use strict'
2
+
3
+ const { join, posix } = require('node:path')
4
+ const { readFile: read, writeFile: write } = require('node:fs/promises')
5
+
6
+ const { hash } = require('@toa.io/generic')
7
+ const { directory } = require('@toa.io/filesystem')
8
+
9
+ /**
10
+ * @implements {toa.deployment.images.Image}
11
+ * @abstract
12
+ */
13
+ class Image {
14
+ context
15
+ reference
16
+
17
+ /**
18
+ * @protected
19
+ * @type {string}
20
+ */
21
+ dockerfile
22
+
23
+ /** @type {string} */
24
+ #scope
25
+ /** @type {toa.norm.context.Runtime} */
26
+ #runtime
27
+ /** @type {string} */
28
+ #type
29
+
30
+ /**
31
+ * @param scope {string}
32
+ * @param runtime {toa.norm.context.Runtime}
33
+ */
34
+ constructor (scope, runtime) {
35
+ this.#scope = scope
36
+ this.#runtime = runtime
37
+ this.#type = this.constructor.name.toLowerCase()
38
+ }
39
+
40
+ /**
41
+ * @abstract
42
+ * @protected
43
+ * @type {string}
44
+ */
45
+ get name () {}
46
+
47
+ /**
48
+ * @abstract
49
+ * @protected
50
+ * @type {string}
51
+ */
52
+ get version () {}
53
+
54
+ tag (base) {
55
+ const tag = hash(this.#runtime.version + ';' + this.version)
56
+
57
+ this.reference = posix.join(base, `${this.#scope}/${this.#type}-${this.name}:${tag}`)
58
+ }
59
+
60
+ async prepare (root) {
61
+ if (this.dockerfile === undefined) throw new Error('Dockerfile isn\'t specified')
62
+
63
+ const path = join(root, `${this.#type}-${this.name}.${this.version}`)
64
+
65
+ await directory.ensure(path)
66
+
67
+ const template = await read(this.dockerfile, 'utf-8')
68
+ const contents = template.replace(/{{(\.?\w+)}}/g, (_, key) => this.#value(key))
69
+ const ignore = 'Dockerfile'
70
+
71
+ await write(join(path, 'Dockerfile'), contents)
72
+ await write(join(path, '.dockerignore'), ignore)
73
+
74
+ this.context = path
75
+
76
+ return path
77
+ }
78
+
79
+ /**
80
+ * @param key {string}
81
+ * @returns {string}
82
+ */
83
+ #value (key) {
84
+ const [, property] = key.split('.')
85
+
86
+ if (property !== undefined) return this[property]
87
+ else return this.#runtime[key]
88
+ }
89
+ }
90
+
91
+ exports.Image = Image
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { Factory } = require('./factory')
4
+ const { Registry } = require('./registry')
5
+
6
+ exports.Factory = Factory
7
+ exports.Registry = Registry
@@ -0,0 +1,102 @@
1
+ 'use strict'
2
+
3
+ const { directory: { remove } } = require('@toa.io/filesystem')
4
+
5
+ /**
6
+ * @implements {toa.deployment.images.Registry}
7
+ */
8
+ class Registry {
9
+ /** @type {toa.norm.context.Registry} */
10
+ #registry
11
+ /** @type {toa.deployment.images.Factory} */
12
+ #factory
13
+ /** @type {toa.operations.Process} */
14
+ #process
15
+ /** @type {Array<toa.deployment.images.Image>} */
16
+ #images = []
17
+
18
+ /**
19
+ * @param {toa.norm.context.Registry} registry
20
+ * @param {toa.deployment.images.Factory} factory
21
+ * @param {toa.operations.Process} process
22
+ */
23
+ constructor (registry, factory, process) {
24
+ this.#registry = registry
25
+ this.#factory = factory
26
+ this.#process = process
27
+ }
28
+
29
+ composition (composition) {
30
+ return this.#create('composition', composition)
31
+ }
32
+
33
+ service (path, service) {
34
+ return this.#create('service', path, service)
35
+ }
36
+
37
+ async prepare (target) {
38
+ await Promise.all(this.#images.map((image) => image.prepare(target)))
39
+ }
40
+
41
+ async push () {
42
+ for (const image of this.#images) {
43
+ await this.#push(image)
44
+ await remove(image.context)
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {"composition" | "service"} type
50
+ * @param {...any} args
51
+ * @returns {toa.deployment.images.Image}
52
+ */
53
+ #create (type, ...args) {
54
+ const image = this.#factory[type](...args)
55
+
56
+ image.tag(this.#registry.base)
57
+ this.#images.push(image)
58
+
59
+ return image
60
+ }
61
+
62
+ /**
63
+ * @param {toa.deployment.images.Image} image
64
+ * @returns {Promise<void>}
65
+ */
66
+ async #push (image) {
67
+ const args = ['buildx', 'build', '--push', '--tag', image.reference, image.context]
68
+ const local = this.#registry.platforms === null
69
+
70
+ if (local) {
71
+ args.push('--builder', 'default')
72
+ } else {
73
+ const platform = this.#registry.platforms.join(',')
74
+
75
+ args.push('--platform', platform)
76
+ args.push('--builder', BUILDER)
77
+
78
+ await this.#builder()
79
+ }
80
+
81
+ await this.#process.execute('docker', args)
82
+ }
83
+
84
+ async #builder () {
85
+ const ls = 'buildx ls'.split(' ')
86
+ const output = await this.#process.execute('docker', ls, { silently: true })
87
+
88
+ const exists = output.split('\n').find((line) => line.startsWith('toa '))
89
+
90
+ if (exists === undefined) {
91
+ const create = `buildx create --name ${BUILDER} --use`.split(' ')
92
+ const bootstrap = 'buildx inspect --bootstrap'.split(' ')
93
+
94
+ await this.#process.execute('docker', create)
95
+ await this.#process.execute('docker', bootstrap)
96
+ }
97
+ }
98
+ }
99
+
100
+ const BUILDER = 'toa'
101
+
102
+ exports.Registry = Registry
@@ -0,0 +1,12 @@
1
+ FROM node:18.2.0-alpine3.15
2
+
3
+ ENV NODE_ENV=production
4
+ RUN npm set registry {{registry}}
5
+ RUN npm set proxy {{proxy}}
6
+ RUN npm i -g @toa.io/runtime@{{version}}
7
+
8
+ WORKDIR /service
9
+ ADD . .
10
+ RUN npm i
11
+
12
+ CMD toa serve .
@@ -0,0 +1,70 @@
1
+ 'use strict'
2
+
3
+ const { join, dirname } = require('node:path')
4
+
5
+ const { Image } = require('./image')
6
+ const { directory: { copy } } = require('@toa.io/filesystem')
7
+
8
+ class Service extends Image {
9
+ dockerfile = join(__dirname, 'service.Dockerfile')
10
+
11
+ /**
12
+ * Used by Dockerfile
13
+ *
14
+ * @readonly
15
+ * @type {string}
16
+ * */
17
+ service
18
+
19
+ /** @type {string} */
20
+ #group
21
+ /** @type {string} */
22
+ #name
23
+ /** @type {string} */
24
+ #path
25
+ /** @type {string} */
26
+ #version
27
+
28
+ /**
29
+ * @param {string} scope
30
+ * @param {toa.norm.context.Runtime} runtime
31
+ * @param {string} reference
32
+ * @param {toa.deployment.dependency.Service} service
33
+ */
34
+ constructor (scope, runtime, reference, service) {
35
+ super(scope, runtime)
36
+
37
+ this.service = service.name
38
+
39
+ this.#group = service.group
40
+ this.#name = service.name
41
+ this.#path = find(reference)
42
+ this.#version = service.version
43
+ }
44
+
45
+ get name () {
46
+ return this.#group + '-' + this.#name
47
+ }
48
+
49
+ get version () {
50
+ return this.#version
51
+ }
52
+
53
+ async prepare (root) {
54
+ const context = await super.prepare(root)
55
+
56
+ await copy(this.#path, context)
57
+
58
+ return context
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {string} reference
64
+ * @returns {string}
65
+ */
66
+ const find = (reference) => {
67
+ return dirname(require.resolve(join(reference, 'package.json')))
68
+ }
69
+
70
+ exports.Service = Service
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const { Factory } = require('./factory')
4
+
5
+ exports.Factory = Factory
@@ -0,0 +1,85 @@
1
+ 'use strict'
2
+
3
+ const { directory } = require('@toa.io/filesystem')
4
+
5
+ /**
6
+ * @implements {toa.deployment.Operator}
7
+ */
8
+ class Operator {
9
+ /** @type {toa.deployment.Deployment} */
10
+ #deployment
11
+ /** @type {toa.deployment.images.Registry} */
12
+ #registry
13
+
14
+ /**
15
+ * @param deployment {toa.deployment.Deployment}
16
+ * @param registry {toa.deployment.images.Registry}
17
+ */
18
+ constructor (deployment, registry) {
19
+ this.#deployment = deployment
20
+ this.#registry = registry
21
+ }
22
+
23
+ async export (path) {
24
+ const target = await Operator.#target('deployment', path)
25
+
26
+ await this.#deployment.export(target)
27
+
28
+ return target
29
+ }
30
+
31
+ async prepare (path) {
32
+ const target = await Operator.#target('images', path)
33
+
34
+ await this.#registry.prepare(target)
35
+
36
+ return target
37
+ }
38
+
39
+ async build () {
40
+ const target = await Operator.#target('images')
41
+
42
+ await this.#registry.prepare(target)
43
+ await this.#registry.push()
44
+
45
+ await directory.remove(target)
46
+ }
47
+
48
+ async install (options = {}) {
49
+ options = Object.assign({}, OPTIONS, options)
50
+
51
+ const [source] = await Promise.all([this.export(), this.build()])
52
+
53
+ await this.#deployment.install(options)
54
+
55
+ await directory.remove(source)
56
+ }
57
+
58
+ async template (options = {}) {
59
+ const source = await this.export()
60
+ const output = await this.#deployment.template(options)
61
+
62
+ await directory.remove(source)
63
+
64
+ return output
65
+ }
66
+
67
+ /**
68
+ * @param type {string}
69
+ * @param [path] {string}
70
+ * @returns {Promise<string>}
71
+ */
72
+ static async #target (type, path) {
73
+ if (path === undefined) path = await directory.temp('toa-' + type)
74
+ else path = await directory.ensure(path)
75
+
76
+ return path
77
+ }
78
+ }
79
+
80
+ /** @type {toa.deployment.installation.Options} */
81
+ const OPTIONS = {
82
+ wait: false
83
+ }
84
+
85
+ exports.Operator = Operator
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ // noinspection JSClosureCompilerSyntax
4
+ /**
5
+ * @implements {toa.deployment.Service}
6
+ */
7
+ class Service {
8
+ name
9
+ image
10
+ port
11
+ ingress
12
+
13
+ /**
14
+ * @param service {toa.deployment.dependency.Service}
15
+ * @param image {toa.deployment.images.Image}
16
+ */
17
+ constructor (service, image) {
18
+ this.name = service.group + '-' + service.name
19
+ this.port = service.port
20
+ this.image = image.reference
21
+ this.ingress = service.ingress
22
+ }
23
+ }
24
+
25
+ exports.Service = Service
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')
package/src/process.js ADDED
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ const execa = require('execa')
4
+
5
+ /**
6
+ * @implements {toa.operations.Process}
7
+ */
8
+ class Process {
9
+ async execute (cmd, args, options = {}) {
10
+ /** @type {execa.ExecaReturnValue<import('stream').Stream>} */
11
+ const command = execa(cmd, args)
12
+
13
+ if (options.silently !== true) {
14
+ command.stdout.pipe(process.stdout)
15
+ command.stderr.pipe(process.stderr)
16
+ }
17
+
18
+ /** @type {execa.ExecaReturnValue<string>} */
19
+ const result = await command
20
+
21
+ return result.stdout
22
+ }
23
+ }
24
+
25
+ exports.Process = Process
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+
5
+ const context = /** @type {toa.norm.Context} */ { name: generate() }
6
+ const compositions = []
7
+ const dependencies = []
8
+ const process = /** @type {toa.operations.Process} */ { execute: jest.fn() }
9
+ const options = /** @type {toa.deployment.installation.Options} */ {
10
+ namespace: generate(),
11
+ target: generate()
12
+ }
13
+
14
+ exports.context = context
15
+ exports.compositions = compositions
16
+ exports.dependencies = dependencies
17
+ exports.process = process
18
+ exports.options = options
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+
5
+ const fixtures = require('./deployment.fixtures')
6
+ const { Deployment } = require('../../src/deployment/deployment')
7
+
8
+ /** @type {toa.deployment.Deployment} */
9
+ let deployment
10
+ /** @type {toa.deployment.installation.Options} */
11
+ let options
12
+
13
+ beforeEach(() => {
14
+ deployment = new Deployment(fixtures.context, fixtures.compositions, fixtures.dependencies, fixtures.process)
15
+ options = clone(fixtures.options)
16
+ })
17
+
18
+ it('should pass -n argument if options.namespace is set', async () => {
19
+ await deployment.install(options)
20
+
21
+ const call = fixtures.process.execute.mock.calls.find((call) => call[0] === 'helm' && call[1][0] === 'upgrade')
22
+
23
+ expect(call).toBeDefined()
24
+
25
+ const args = call[1]
26
+ const index = args.findIndex((value) => value === '-n')
27
+ const namespace = args[index + 1]
28
+
29
+ expect(index).not.toStrictEqual(-1)
30
+ expect(namespace).toStrictEqual(fixtures.options.namespace)
31
+ })
32
+
33
+ it('should forbid local deployment environment', () => {
34
+ const context = clone(fixtures.context)
35
+
36
+ context.environment = 'local'
37
+
38
+ const create = () => new Deployment(context, fixtures.compositions, fixtures.dependencies, fixtures.process)
39
+
40
+ expect(create).toThrow(/name 'local' is not allowed/)
41
+ })
@@ -0,0 +1,33 @@
1
+ 'use strict'
2
+
3
+ const { Image } = require('../../../src/deployment/images/image')
4
+ const { generate } = require('randomstring')
5
+ const { hash } = require('@toa.io/generic')
6
+
7
+ const version = generate()
8
+ const name = generate()
9
+
10
+ /**
11
+ * @implements {toa.deployment.images.Image}
12
+ */
13
+ class Class extends Image {
14
+ get name () {
15
+ return name
16
+ }
17
+
18
+ get version () {
19
+ return version
20
+ }
21
+ }
22
+
23
+ /** @type {toa.norm.context.Runtime} */
24
+ const runtime = {
25
+ version: generate()
26
+ }
27
+
28
+ exports.scope = generate()
29
+ exports.name = name
30
+ exports.version = hash(runtime.version + ';' + version)
31
+ exports.Class = Class
32
+ exports.runtime = runtime
33
+ exports.process = process
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ const fixtures = require('./image.fixtures')
4
+ const { generate } = require('randomstring')
5
+
6
+ /** @type {toa.deployment.images.Image} */
7
+ let instance
8
+
9
+ beforeEach(() => {
10
+ instance = new fixtures.Class(fixtures.scope, fixtures.runtime)
11
+ })
12
+
13
+ it('should assign url', () => {
14
+ const registry = generate()
15
+
16
+ instance.tag(registry)
17
+
18
+ expect(instance.reference).toEqual(`${registry}/${fixtures.scope}/class-${fixtures.name}:${fixtures.version}`)
19
+ })
20
+
21
+ describe('prepare', () => {
22
+ it('should throw error if no dockerfile specified', async () => {
23
+ await expect(instance.prepare(generate())).rejects.toThrow(/Dockerfile isn't specified/)
24
+ })
25
+ })
@@ -0,0 +1,13 @@
1
+ // noinspection ES6UnusedImports
2
+
3
+ import type { Deployable } from './deployment'
4
+
5
+ declare namespace toa.deployment {
6
+
7
+ interface Composition extends Deployable {
8
+ components: Array<string>
9
+ }
10
+
11
+ }
12
+
13
+ export type Composition = toa.deployment.Composition
@@ -0,0 +1,77 @@
1
+ // noinspection JSUnusedGlobalSymbols,ES6UnusedImports
2
+
3
+ import { Service } from './service'
4
+ import { dependencies } from '@toa.io/norm/types/context'
5
+
6
+ declare namespace toa.deployment {
7
+
8
+ namespace dependency {
9
+
10
+ type Constructor = (instances: dependencies.Instance[], annotation: any) => Declaration
11
+
12
+ type Reference = {
13
+ name: string
14
+ version: string
15
+ repository?: string
16
+ alias?: string
17
+ values?: Object
18
+ }
19
+
20
+ type Service = {
21
+ group: string
22
+ name: string
23
+ version: string
24
+ port: number
25
+ ingress?: {
26
+ host: string
27
+ class: string
28
+ annotations?: object
29
+ }
30
+ }
31
+
32
+ type Proxy = {
33
+ name: string
34
+ target: string
35
+ }
36
+
37
+ type Variable = {
38
+ name: string
39
+ value?: string | number
40
+ secret?: {
41
+ name: string,
42
+ key: string
43
+ }
44
+ }
45
+
46
+ type Variables = {
47
+ [key: string]: Variable[]
48
+ }
49
+
50
+ type Declaration = {
51
+ references?: Reference[]
52
+ services?: Service[] // dependency.Service
53
+ proxies?: Proxy[]
54
+ variables?: Variables
55
+ }
56
+
57
+ }
58
+
59
+ type Dependency = {
60
+ references?: dependency.Reference[]
61
+ services?: Service[] // deployment.Service
62
+ proxies?: dependency.Proxy[]
63
+ variables?: dependency.Variables
64
+ }
65
+
66
+ }
67
+
68
+ export namespace dependency {
69
+ export type Declaration = toa.deployment.dependency.Declaration
70
+ export type Reference = toa.deployment.dependency.Reference
71
+ export type Service = toa.deployment.dependency.Service
72
+ export type Proxy = toa.deployment.dependency.Proxy
73
+ export type Variables = toa.deployment.dependency.Variables
74
+ export type Variable = toa.deployment.dependency.Variable
75
+ }
76
+
77
+ export type Dependency = toa.deployment.Dependency