@toa.io/operations 1.0.0-alpha.317 → 1.0.0-alpha.319
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/CHANGELOG.md +14 -0
- package/package.json +3 -3
- package/readme.md +25 -0
- package/src/deployment/deployment.js +14 -0
- package/src/deployment/images/bundle.js +3 -2
- package/src/deployment/operator.js +3 -0
- package/src/deployment/operator.test.js +41 -0
- package/src/deployment/secrets.js +92 -0
- package/src/deployment/secrets.test.js +172 -0
- package/types/_deployment/dependency.d.ts +2 -0
- package/types/_deployment/deployment.d.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.0.0-alpha.319](https://github.com/toa-io/toa/compare/v1.0.0-alpha.318...v1.0.0-alpha.319) (2026-09-27)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **operations:** refuse a deploy where the cluster does not hold a secret ([beb7ccf](https://github.com/toa-io/toa/commit/beb7ccf35619494ab9ad6659963969bb4f9603c8))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# [1.0.0-alpha.318](https://github.com/toa-io/toa/compare/v1.0.0-alpha.317...v1.0.0-alpha.318) (2026-09-23)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **deployment:** copy a component's linked sources as files ([1a1a8c1](https://github.com/toa-io/toa/commit/1a1a8c10684348dba02b475ab809a4083f86a601))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# [1.0.0-alpha.317](https://github.com/toa-io/toa/compare/v1.0.0-alpha.316...v1.0.0-alpha.317) (2026-09-23)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @toa.io/operations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.319",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Deployment",
|
|
6
6
|
"homepage": "https://toa.io",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@toa.io/generic": "1.0.0-alpha.311",
|
|
32
32
|
"@toa.io/kubernetes": "1.0.0-alpha.301",
|
|
33
|
-
"@toa.io/norm": "1.0.0-alpha.
|
|
33
|
+
"@toa.io/norm": "1.0.0-alpha.319",
|
|
34
34
|
"oxc-parser": "0.150.0",
|
|
35
35
|
"paseto": "4.0.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "ee4901f2bc12c33fc2098a62f2af0c29838a2c2c"
|
|
38
38
|
}
|
package/readme.md
CHANGED
|
@@ -60,6 +60,23 @@ declared. `evicted@production` states it for one environment.
|
|
|
60
60
|
|
|
61
61
|
See [compositions](../documentation/compositions.md#evicted).
|
|
62
62
|
|
|
63
|
+
## Secrets
|
|
64
|
+
|
|
65
|
+
A value that must not be in the chart is rendered as a reference to a Kubernetes secret, and the
|
|
66
|
+
secret is the application's to deploy. A deploy reads the namespace it deploys into before it builds
|
|
67
|
+
anything, and refuses where a key a workload would read is not there:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ toa deploy production
|
|
71
|
+
Secrets are not deployed: toa-mongodb.default/username, toa-mongodb.default/password
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Required is what `toa export secrets` lists, less the keys it marks `(optional)`, plus the
|
|
75
|
+
`registry.credentials` secret where a context names one. `toa deploy --dry` reads no cluster, and
|
|
76
|
+
neither does anything else that reads a context: `toa env`, `toa export`, `toa build` and `toa push`.
|
|
77
|
+
|
|
78
|
+
See [variables and secrets](../documentation/deployment.md#variables-and-secrets).
|
|
79
|
+
|
|
63
80
|
## Context
|
|
64
81
|
|
|
65
82
|
### Container Registry
|
|
@@ -96,6 +113,11 @@ for what the components declare — and is built only when one of those changes.
|
|
|
96
113
|
layer, so a deploy that changes code alone builds and pushes that layer, and neither
|
|
97
114
|
downloads nor uploads the dependencies again. The same holds for `mono`.
|
|
98
115
|
|
|
116
|
+
The sources are copied as files, so a component that links a directory in — sources it shares with
|
|
117
|
+
another component — carries what the link points to, and a link the build cannot read fails the
|
|
118
|
+
build. What is linked is source alone: dependencies are installed per component, from its own
|
|
119
|
+
`package.json`.
|
|
120
|
+
|
|
99
121
|
Beside each component's sources the layer carries `manifest.toa.json`, the manifest as this
|
|
100
122
|
build normalised it. A composition reads it instead of normalising again, so it loads no
|
|
101
123
|
bridge to read what a module declares; a workspace has no such file and is read as it always
|
|
@@ -198,6 +220,9 @@ registry:
|
|
|
198
220
|
credentials: docker-credentials-secret-name
|
|
199
221
|
```
|
|
200
222
|
|
|
223
|
+
The secret itself is not Toa's to create, and a deploy that does not find it in the cluster is
|
|
224
|
+
[refused](#secrets).
|
|
225
|
+
|
|
201
226
|
#### Toa images
|
|
202
227
|
|
|
203
228
|
Each release publishes `ghcr.io/toa-io/runtime:<version>` and, beside it, `:alpha` or `:latest`.
|
|
@@ -6,6 +6,7 @@ import { shortcuts } from '@toa.io/norm'
|
|
|
6
6
|
|
|
7
7
|
import { merge, declare, describe } from './.deployment/index.js'
|
|
8
8
|
import { drain } from './drain.js'
|
|
9
|
+
import * as secrets from './secrets.js'
|
|
9
10
|
|
|
10
11
|
export class Deployment {
|
|
11
12
|
#chart
|
|
@@ -47,6 +48,19 @@ export class Deployment {
|
|
|
47
48
|
this.#target = target
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Refuses a deploy the cluster is not ready for: a secret a workload reads that is not there
|
|
53
|
+
* leaves its pod uncreated, which is seen only once every image has been built and pushed.
|
|
54
|
+
*
|
|
55
|
+
* @param {toa.deployment.installation.Options} options
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async verify(options) {
|
|
59
|
+
const references = secrets.references(this.variables(), this.#values.credentials)
|
|
60
|
+
|
|
61
|
+
await secrets.verify(this.#process, references, options)
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
async install(options) {
|
|
51
65
|
if (options.target) this.#target = options.target
|
|
52
66
|
if (this.#target === undefined) throw new Error("Deployment hasn't been exported")
|
|
@@ -75,8 +75,9 @@ export class Bundle extends Image {
|
|
|
75
75
|
for (const component of this.components) {
|
|
76
76
|
const target = join(context, component.locator.label)
|
|
77
77
|
|
|
78
|
-
// what was installed in the workspace is not what the image installs
|
|
79
|
-
|
|
78
|
+
// what was installed in the workspace is not what the image installs; a link is copied as
|
|
79
|
+
// the files it points to, which is the only way what it points to reaches the image at all
|
|
80
|
+
await cp(component.path, target, { recursive: true, filter: sources, dereference: true })
|
|
80
81
|
await declare(component.path, target, component.locator.label)
|
|
81
82
|
await normalized(component, target)
|
|
82
83
|
}
|
|
@@ -40,6 +40,9 @@ export class Operator {
|
|
|
40
40
|
async install(options = {}) {
|
|
41
41
|
options = Object.assign({}, OPTIONS, options)
|
|
42
42
|
|
|
43
|
+
// nothing is built, pushed or applied where the cluster cannot run what this deploys
|
|
44
|
+
await this.#deployment.verify(options)
|
|
45
|
+
|
|
43
46
|
await Promise.all([this.export(), this.push()])
|
|
44
47
|
await this.#registry.alias(this.#environment)
|
|
45
48
|
await this.#deployment.install(options)
|
|
@@ -54,6 +54,7 @@ describe('install', () => {
|
|
|
54
54
|
beforeEach(() => {
|
|
55
55
|
deployment = /** @type {toa.deployment.Deployment} */ {
|
|
56
56
|
export: mock.fn(async () => {}),
|
|
57
|
+
verify: mock.fn(async () => {}),
|
|
57
58
|
install: mock.fn(async () => {})
|
|
58
59
|
}
|
|
59
60
|
registry = /** @type {toa.deployment.Registry} */ {
|
|
@@ -62,6 +63,46 @@ describe('install', () => {
|
|
|
62
63
|
}
|
|
63
64
|
})
|
|
64
65
|
|
|
66
|
+
it('should verify the secrets before anything is built', async () => {
|
|
67
|
+
const order = []
|
|
68
|
+
|
|
69
|
+
deployment.verify = mock.fn(async () => {
|
|
70
|
+
order.push('verify')
|
|
71
|
+
})
|
|
72
|
+
deployment.export = mock.fn(async () => {
|
|
73
|
+
order.push('export')
|
|
74
|
+
})
|
|
75
|
+
registry.push = mock.fn(async () => {
|
|
76
|
+
order.push('push')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
operator = new Operator(deployment, registry, 'production')
|
|
80
|
+
|
|
81
|
+
await operator.install({ namespace: 'acme' })
|
|
82
|
+
|
|
83
|
+
// export and push race each other, and both wait for the cluster to have been read
|
|
84
|
+
assert.strictEqual(order[0], 'verify')
|
|
85
|
+
assert.deepStrictEqual(order.slice(1).sort(), ['export', 'push'])
|
|
86
|
+
assert.deepStrictEqual(deployment.verify.mock.calls[0].arguments, [
|
|
87
|
+
{ wait: false, namespace: 'acme' }
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should build nothing where the secrets are not deployed', async () => {
|
|
92
|
+
deployment.verify = mock.fn(async () => {
|
|
93
|
+
throw new Error('Secrets are not deployed: toa-mongodb.default/username')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
operator = new Operator(deployment, registry, 'production')
|
|
97
|
+
|
|
98
|
+
await assert.rejects(operator.install(), /Secrets are not deployed/)
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(deployment.export.mock.callCount(), 0)
|
|
101
|
+
assert.strictEqual(registry.push.mock.callCount(), 0)
|
|
102
|
+
assert.strictEqual(registry.alias.mock.callCount(), 0)
|
|
103
|
+
assert.strictEqual(deployment.install.mock.callCount(), 0)
|
|
104
|
+
})
|
|
105
|
+
|
|
65
106
|
it('should tag the environment after a push', async () => {
|
|
66
107
|
const order = []
|
|
67
108
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the cluster has to hold for a deployment to run, and the refusal where it does not.
|
|
3
|
+
*
|
|
4
|
+
* A pod whose secret is not there is never created, and nothing the chart carries says so: Helm
|
|
5
|
+
* reports a release, the rollout stalls, and the name of what is missing is in the events of a pod
|
|
6
|
+
* that does not exist. The cluster answers the question before any of that, for one call.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every secret the workloads read, as `<name>/<key>`, and the image pull secret by name alone —
|
|
11
|
+
* what it holds is the registry's affair, and its absence stops a pod just the same.
|
|
12
|
+
*
|
|
13
|
+
* @param {toa.deployment.dependency.Variable[]} variables
|
|
14
|
+
* @param {string} [credentials] `registry.credentials`
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export function references(variables, credentials) {
|
|
18
|
+
/** @type {string[]} */
|
|
19
|
+
const references = []
|
|
20
|
+
|
|
21
|
+
// as the chart renders it: `imagePullSecrets` is there where the name is, and a context
|
|
22
|
+
// that states the key and no name names no secret
|
|
23
|
+
if (credentials) references.push(credentials)
|
|
24
|
+
|
|
25
|
+
for (const { secret } of variables) {
|
|
26
|
+
// a key the workload starts without is not one the cluster has to hold
|
|
27
|
+
if (secret === undefined || secret.optional === true) continue
|
|
28
|
+
|
|
29
|
+
const reference = `${secret.name}/${secret.key}`
|
|
30
|
+
|
|
31
|
+
if (!references.includes(reference)) references.push(reference)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return references
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {toa.operations.Process} process
|
|
39
|
+
* @param {string[]} references
|
|
40
|
+
* @param {toa.deployment.installation.Options} options
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
export async function verify(process, references, options) {
|
|
44
|
+
if (references.length === 0) return
|
|
45
|
+
|
|
46
|
+
const deployed = await read(process, options)
|
|
47
|
+
const missing = references.filter((reference) => !deployed.has(reference))
|
|
48
|
+
|
|
49
|
+
if (missing.length > 0)
|
|
50
|
+
throw new Error(`Secrets are not deployed: ${missing.join(', ')}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* What the namespace holds, as the names of its secrets and the `<name>/<key>` of every key in
|
|
55
|
+
* them. The namespace is read whole: a call per secret would be a process per secret, and the
|
|
56
|
+
* wrapper for a single one cannot tell a secret that is absent from a cluster that cannot be
|
|
57
|
+
* reached — which a preflight is the worst place to guess at. Listing asks no permission a
|
|
58
|
+
* deploy does not already have, Helm keeping its own release state in secrets of this namespace.
|
|
59
|
+
*
|
|
60
|
+
* Only the keys are read. What is under them is the cluster's and stays there.
|
|
61
|
+
*
|
|
62
|
+
* @param {toa.operations.Process} process
|
|
63
|
+
* @param {toa.deployment.installation.Options} options
|
|
64
|
+
* @returns {Promise<Set<string>>}
|
|
65
|
+
*/
|
|
66
|
+
async function read(process, options) {
|
|
67
|
+
const args = ['get', 'secrets', '-o', TEMPLATE]
|
|
68
|
+
|
|
69
|
+
if (options.namespace !== undefined) args.push('-n', options.namespace)
|
|
70
|
+
|
|
71
|
+
const output = await process.execute('kubectl', args, { silently: true })
|
|
72
|
+
|
|
73
|
+
/** @type {Set<string>} */
|
|
74
|
+
const deployed = new Set()
|
|
75
|
+
|
|
76
|
+
for (const line of output.split('\n')) {
|
|
77
|
+
if (line === '') continue
|
|
78
|
+
|
|
79
|
+
const [name, ...keys] = line.split(' ')
|
|
80
|
+
|
|
81
|
+
deployed.add(name)
|
|
82
|
+
|
|
83
|
+
for (const key of keys) deployed.add(`${name}/${key}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return deployed
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A secret's name and the keys in it, one secret a line. Neither may hold a space. */
|
|
90
|
+
const TEMPLATE =
|
|
91
|
+
'go-template={{range .items}}{{.metadata.name}}' +
|
|
92
|
+
'{{range $key, $value := .data}} {{$key}}{{end}}{{"\\n"}}{{end}}'
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { it, describe, beforeEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { references, verify } from './secrets.js'
|
|
5
|
+
|
|
6
|
+
describe('references', () => {
|
|
7
|
+
it('should reference a secret by name and key', () => {
|
|
8
|
+
const variables = [
|
|
9
|
+
{ name: 'TOA_CONTEXT', value: 'todos' },
|
|
10
|
+
{ name: 'USERNAME', secret: { name: 'toa-mongodb.default', key: 'username' } },
|
|
11
|
+
{ name: 'PASSWORD', secret: { name: 'toa-mongodb.default', key: 'password' } }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
assert.deepStrictEqual(references(variables), [
|
|
15
|
+
'toa-mongodb.default/username',
|
|
16
|
+
'toa-mongodb.default/password'
|
|
17
|
+
])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should not reference an optional key', () => {
|
|
21
|
+
const variables = [
|
|
22
|
+
{ name: 'KEY', secret: { name: 'toa-tokens', key: 'key0', optional: true } },
|
|
23
|
+
{ name: 'USERNAME', secret: { name: 'toa-mongodb.default', key: 'username' } }
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
assert.deepStrictEqual(references(variables), ['toa-mongodb.default/username'])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should reference a key read by two workloads once', () => {
|
|
30
|
+
const secret = { name: 'toa-mongodb.default', key: 'username' }
|
|
31
|
+
|
|
32
|
+
assert.deepStrictEqual(references([{ name: 'A', secret }, { name: 'B', secret }]), [
|
|
33
|
+
'toa-mongodb.default/username'
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should reference the image pull secret by name', () => {
|
|
38
|
+
const variables = [
|
|
39
|
+
{ name: 'USERNAME', secret: { name: 'toa-mongodb.default', key: 'username' } }
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
assert.deepStrictEqual(references(variables, 'registry-credentials'), [
|
|
43
|
+
'registry-credentials',
|
|
44
|
+
'toa-mongodb.default/username'
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should reference nothing where nothing is secret', () => {
|
|
49
|
+
assert.deepStrictEqual(references([{ name: 'TOA_ENV', value: 'production' }]), [])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should reference no pull secret where a context names none', () => {
|
|
53
|
+
// as the chart reads it: `credentials: ~` renders no `imagePullSecrets`
|
|
54
|
+
assert.deepStrictEqual(references([], null), [])
|
|
55
|
+
assert.deepStrictEqual(references([], ''), [])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('verify', () => {
|
|
60
|
+
/** @type {string} what `kubectl` answers */
|
|
61
|
+
let secrets
|
|
62
|
+
|
|
63
|
+
/** @type {toa.operations.Process} */
|
|
64
|
+
let process
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
secrets = ''
|
|
68
|
+
process = /** @type {toa.operations.Process} */ {
|
|
69
|
+
execute: mock.fn(async () => secrets)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should read the namespace deployed into', async () => {
|
|
74
|
+
secrets = 'toa-mongodb.default password username'
|
|
75
|
+
|
|
76
|
+
await verify(process, ['toa-mongodb.default/username'], { namespace: 'acme' })
|
|
77
|
+
|
|
78
|
+
const [cmd, args, options] = process.execute.mock.calls[0].arguments
|
|
79
|
+
|
|
80
|
+
assert.strictEqual(cmd, 'kubectl')
|
|
81
|
+
assert.deepStrictEqual(args.slice(0, 2), ['get', 'secrets'])
|
|
82
|
+
assert.deepStrictEqual(args.slice(-2), ['-n', 'acme'])
|
|
83
|
+
assert.deepStrictEqual(options, { silently: true })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should read the current namespace where none is given', async () => {
|
|
87
|
+
await verify(process, [], {})
|
|
88
|
+
|
|
89
|
+
assert.strictEqual(process.execute.mock.callCount(), 0)
|
|
90
|
+
|
|
91
|
+
await verify(process, ['toa-mongodb.default/username'], {}).catch(() => {})
|
|
92
|
+
|
|
93
|
+
const [, args] = process.execute.mock.calls[0].arguments
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(args.includes('-n'), false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should name every key that is not deployed', async () => {
|
|
99
|
+
secrets = 'toa-amqp-context.default password username'
|
|
100
|
+
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
verify(
|
|
103
|
+
process,
|
|
104
|
+
[
|
|
105
|
+
'toa-amqp-context.default/username',
|
|
106
|
+
'toa-mongodb.default/username',
|
|
107
|
+
'toa-mongodb.default/password'
|
|
108
|
+
],
|
|
109
|
+
{}
|
|
110
|
+
),
|
|
111
|
+
{
|
|
112
|
+
message:
|
|
113
|
+
'Secrets are not deployed: toa-mongodb.default/username, ' +
|
|
114
|
+
'toa-mongodb.default/password'
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should name the key a deployed secret lacks', async () => {
|
|
120
|
+
secrets = 'toa-mongodb.default username'
|
|
121
|
+
|
|
122
|
+
await assert.rejects(
|
|
123
|
+
verify(process, ['toa-mongodb.default/username', 'toa-mongodb.default/password'], {}),
|
|
124
|
+
{ message: 'Secrets are not deployed: toa-mongodb.default/password' }
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should name a secret referenced by name alone', async () => {
|
|
129
|
+
secrets = 'toa-mongodb.default password username'
|
|
130
|
+
|
|
131
|
+
await assert.rejects(verify(process, ['registry-credentials'], {}), {
|
|
132
|
+
message: 'Secrets are not deployed: registry-credentials'
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
secrets = 'registry-credentials .dockerconfigjson'
|
|
136
|
+
|
|
137
|
+
await verify(process, ['registry-credentials'], {})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should pass where the cluster holds every key', async () => {
|
|
141
|
+
secrets =
|
|
142
|
+
'default-token\ntoa-amqp-context.default password username\n' +
|
|
143
|
+
'toa-mongodb.default password username'
|
|
144
|
+
|
|
145
|
+
await verify(
|
|
146
|
+
process,
|
|
147
|
+
[
|
|
148
|
+
'toa-amqp-context.default/username',
|
|
149
|
+
'toa-amqp-context.default/password',
|
|
150
|
+
'toa-mongodb.default/username',
|
|
151
|
+
'toa-mongodb.default/password'
|
|
152
|
+
],
|
|
153
|
+
{}
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should not read the cluster where nothing is secret', async () => {
|
|
158
|
+
await verify(process, [], { namespace: 'acme' })
|
|
159
|
+
|
|
160
|
+
assert.strictEqual(process.execute.mock.callCount(), 0)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should fail as the error it is where the cluster cannot be read', async () => {
|
|
164
|
+
process.execute = mock.fn(async () => {
|
|
165
|
+
throw new Error('Command failed with exit code 1: kubectl get secrets')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await assert.rejects(verify(process, ['toa-mongodb.default/username'], {}), {
|
|
169
|
+
message: /^Command failed/
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
@@ -45,6 +45,9 @@ declare namespace toa.deployment {
|
|
|
45
45
|
interface Deployment {
|
|
46
46
|
export(target: string): Promise<void>
|
|
47
47
|
|
|
48
|
+
/** Raises where the cluster does not hold a secret the workloads read. */
|
|
49
|
+
verify(options: installation.Options): Promise<void>
|
|
50
|
+
|
|
48
51
|
install(options: installation.Options): Promise<void>
|
|
49
52
|
|
|
50
53
|
template(options: template.Options): Promise<string>
|