@toa.io/extensions.exposition 1.0.0-alpha.273 → 1.0.0-alpha.276
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 +209 -0
- package/components/identity.basic/manifest.toa.yaml +1 -1
- package/components/identity.basic/operations/authenticate.js +1 -1
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/transit.js +1 -1
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/operations/types.d.ts +2 -2
- package/components/identity.basic/source/authenticate.ts +1 -1
- package/components/identity.basic/source/transit.ts +1 -1
- package/components/identity.basic/source/types.ts +2 -2
- package/components/identity.credentials/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/lib/exchange.js +2 -2
- package/components/identity.federation/operations/lib/exchange.js.map +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/types/configuration.d.ts +3 -2
- package/components/identity.federation/source/lib/exchange.ts +2 -2
- package/components/identity.federation/source/types/configuration.ts +4 -2
- package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/operations/decrypt.js +1 -1
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/encrypt.js +1 -1
- package/components/identity.tokens/operations/encrypt.js.map +1 -1
- package/components/identity.tokens/operations/lib/types.d.ts +3 -2
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/source/authenticate.test.ts +7 -2
- package/components/identity.tokens/source/decrypt.test.ts +13 -7
- package/components/identity.tokens/source/decrypt.ts +1 -1
- package/components/identity.tokens/source/encrypt.test.ts +9 -4
- package/components/identity.tokens/source/encrypt.ts +1 -1
- package/components/identity.tokens/source/lib/types.ts +3 -2
- package/context.toa.yaml +2 -2
- package/documentation/components.md +1 -1
- package/documentation/protocol.md +24 -0
- package/features/configuration.feature +207 -0
- package/features/identity.federation.feature +15 -7
- package/features/introspection.map.feature +16 -0
- package/features/octets.cloudinary.feature +8 -2
- package/features/octets.download.feature +3 -1
- package/features/probes.feature +8 -4
- package/features/steps/Common.ts +24 -1
- package/features/steps/Components.ts +29 -0
- package/features/steps/Gateway.ts +57 -11
- package/features/steps/Parameters.ts +4 -0
- package/features/steps/Probe.ts +34 -0
- package/package.json +4 -3
- package/readme.md +7 -13
- package/schemas/annotation.cos.yaml +11 -0
- package/source/Annotation.ts +11 -0
- package/source/HTTP/Context.ts +1 -6
- package/source/HTTP/Probe.ts +112 -0
- package/source/HTTP/Server.ts +123 -74
- package/source/HTTP/Timing.ts +1 -1
- package/source/HTTP/formats/yaml.test.ts +6 -0
- package/source/HTTP/formats/yaml.ts +21 -1
- package/source/HTTP/index.ts +2 -0
- package/source/HTTP/messages.test.ts +2 -5
- package/source/HTTP/messages.ts +38 -42
- package/source/HTTP/types.ts +39 -0
- package/source/deployment.ts +12 -4
- package/source/directives/octets/Get.ts +3 -3
- package/transpiled/Annotation.d.ts +10 -0
- package/transpiled/HTTP/Context.d.ts +1 -6
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Probe.d.ts +36 -0
- package/transpiled/HTTP/Probe.js +117 -0
- package/transpiled/HTTP/Probe.js.map +1 -0
- package/transpiled/HTTP/Server.d.ts +11 -7
- package/transpiled/HTTP/Server.js +92 -58
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/Timing.d.ts +1 -2
- package/transpiled/HTTP/formats/yaml.js +15 -1
- package/transpiled/HTTP/formats/yaml.js.map +1 -1
- package/transpiled/HTTP/index.d.ts +2 -0
- package/transpiled/HTTP/index.js +2 -0
- package/transpiled/HTTP/index.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +8 -3
- package/transpiled/HTTP/messages.js +31 -37
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/HTTP/types.d.ts +40 -0
- package/transpiled/HTTP/types.js +3 -0
- package/transpiled/HTTP/types.js.map +1 -0
- package/transpiled/deployment.js +8 -2
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/octets/Get.js +3 -3
- package/transpiled/directives/octets/Get.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
|
@@ -14,6 +14,7 @@ let instance: Connector | null = null
|
|
|
14
14
|
@binding()
|
|
15
15
|
export class Gateway {
|
|
16
16
|
private default: boolean = true
|
|
17
|
+
private written: string[] = []
|
|
17
18
|
|
|
18
19
|
@given('the annotation:')
|
|
19
20
|
public async annotate (yaml: string): Promise<void> {
|
|
@@ -47,7 +48,15 @@ export class Gateway {
|
|
|
47
48
|
*/
|
|
48
49
|
@given('the annotation of the introspection map')
|
|
49
50
|
public async annotateMap (): Promise<void> {
|
|
50
|
-
|
|
51
|
+
await this.annotateMapUnder()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The same routes under a root of the scenario's own: what an application declares around the map. */
|
|
55
|
+
@given('the annotation of the introspection map under:')
|
|
56
|
+
public async annotateMapUnder (yaml?: string): Promise<void> {
|
|
57
|
+
const tree: syntax.Node = yaml === undefined
|
|
58
|
+
? { routes: [], methods: [], directives: [] }
|
|
59
|
+
: syntax.parse((parse(yaml) as { '/': object })['/'], shortcuts)
|
|
51
60
|
|
|
52
61
|
for (const manifest of await manifests()) {
|
|
53
62
|
const node = manifest.extensions?.[EXPOSITION] as syntax.Node | undefined
|
|
@@ -74,6 +83,9 @@ export class Gateway {
|
|
|
74
83
|
const patch = parse(yaml) as object
|
|
75
84
|
const configuration = Object.assign({}, def, patch)
|
|
76
85
|
|
|
86
|
+
// scenario-scoped, as the secrets it may refer to are: a configuration left behind
|
|
87
|
+
// outlives the secrets it points at, and the next scenario cannot resolve them
|
|
88
|
+
this.written.push(key)
|
|
77
89
|
process.env[key] = JSON.stringify(configuration)
|
|
78
90
|
|
|
79
91
|
await Gateway.stop()
|
|
@@ -81,6 +93,21 @@ export class Gateway {
|
|
|
81
93
|
this.default = false
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
/** The secrets a scenario's configuration refers to. */
|
|
97
|
+
@given('the configuration secrets:')
|
|
98
|
+
public async secrets (yaml: string): Promise<void> {
|
|
99
|
+
const secrets = parse(yaml) as Record<string, string>
|
|
100
|
+
|
|
101
|
+
for (const [name, value] of Object.entries(secrets)) {
|
|
102
|
+
this.written.push(SECRET + name)
|
|
103
|
+
process.env[SECRET + name] = value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await Gateway.stop()
|
|
107
|
+
|
|
108
|
+
this.default = false
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
@given('the branch TTL is {float} second(s)')
|
|
85
112
|
public async setBranchTTL (seconds: number): Promise<void> {
|
|
86
113
|
process.env.__TESTING_EXPOSITION_BRANCH_TTL = String(seconds * 1000)
|
|
@@ -116,6 +143,11 @@ export class Gateway {
|
|
|
116
143
|
public async cleanup (): Promise<void> {
|
|
117
144
|
delete process.env.__TESTING_EXPOSITION_BRANCH_TTL
|
|
118
145
|
|
|
146
|
+
for (const key of this.written)
|
|
147
|
+
delete process.env[key]
|
|
148
|
+
|
|
149
|
+
this.written = []
|
|
150
|
+
|
|
119
151
|
if (this.default)
|
|
120
152
|
return
|
|
121
153
|
|
|
@@ -138,10 +170,14 @@ export class Gateway {
|
|
|
138
170
|
|
|
139
171
|
process.env[key] ??= JSON.stringify(configuration)
|
|
140
172
|
}
|
|
173
|
+
|
|
174
|
+
for (const [name, value] of Object.entries(DEFAULT_SECRETS))
|
|
175
|
+
process.env[SECRET + name] ??= value
|
|
141
176
|
}
|
|
142
177
|
}
|
|
143
178
|
|
|
144
179
|
const EXPOSITION = '@toa.io/extensions.exposition'
|
|
180
|
+
const SECRET = 'TOA_CONFIGURATION__'
|
|
145
181
|
|
|
146
182
|
const DEFAULT_TREE = JSON.stringify({
|
|
147
183
|
routes: [],
|
|
@@ -158,21 +194,31 @@ const DEFAULT_TREE = JSON.stringify({
|
|
|
158
194
|
const DEFAULT_PROPERTIES: Partial<http.Options> = {
|
|
159
195
|
authorities: {
|
|
160
196
|
nex: 'nex.toa.io'
|
|
161
|
-
}
|
|
197
|
+
},
|
|
198
|
+
// `npm run features:h2c` runs the whole suite over cleartext HTTP/2
|
|
199
|
+
protocol: process.env.TOA_EXPOSITION_PROTOCOL === 'h2c' ? 'h2c' : 'h1'
|
|
162
200
|
}
|
|
163
201
|
|
|
202
|
+
// the identity components boot inside the gateway, and without a variable each would wait
|
|
203
|
+
// for the values service, which these features do not run
|
|
164
204
|
const DEFAULT_CONFIGURATION: Record<string, object> = {
|
|
205
|
+
// a component declaring configuration waits for `configuration.values`, which these
|
|
206
|
+
// features do not run; the variable is the local override that stands in for it
|
|
207
|
+
'realtime.streams': {},
|
|
208
|
+
'identity.basic': {},
|
|
209
|
+
'identity.federation': {},
|
|
210
|
+
'identity.otp': {},
|
|
211
|
+
'identity.passkeys': {},
|
|
165
212
|
'identity.tokens': {
|
|
166
213
|
keys: [
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
key: 'sTxL6qVOadKkUJwh3FveU53XgTEo3Sdfg7k2FfiIKfs'
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
id: 'legacy0',
|
|
173
|
-
key: 'k3.local.pIZT8-9Fa6U_QtfQHOSStfGtmyzPINyKQq2Xk-hd7vA',
|
|
174
|
-
format: 'paseto'
|
|
175
|
-
}
|
|
214
|
+
{ id: 'key0', key: '$IDENTITY_TOKENS_KEY0' },
|
|
215
|
+
{ id: 'legacy0', key: '$IDENTITY_TOKENS_LEGACY0', format: 'paseto' }
|
|
176
216
|
]
|
|
177
217
|
}
|
|
178
218
|
}
|
|
219
|
+
|
|
220
|
+
// a secret is given as a reference, and comes to the component as an object
|
|
221
|
+
const DEFAULT_SECRETS: Record<string, string> = {
|
|
222
|
+
IDENTITY_TOKENS_KEY0: 'sTxL6qVOadKkUJwh3FveU53XgTEo3Sdfg7k2FfiIKfs',
|
|
223
|
+
IDENTITY_TOKENS_LEGACY0: 'k3.local.pIZT8-9Fa6U_QtfQHOSStfGtmyzPINyKQq2Xk-hd7vA'
|
|
224
|
+
}
|
|
@@ -16,6 +16,10 @@ setDefaultTimeout(60 * 1000)
|
|
|
16
16
|
|
|
17
17
|
process.env.TOA_DEV = '1'
|
|
18
18
|
|
|
19
|
+
// the gateway answers for itself, as it does in a deployment: telemetry's probe tracks the
|
|
20
|
+
// nested composition, which connects before route discovery settles
|
|
21
|
+
process.env.TOA_TELEMETRY_READY ??= JSON.stringify(false)
|
|
22
|
+
|
|
19
23
|
// export traces to the local Tempo (`docker compose up tempo grafana`),
|
|
20
24
|
// unavailability of the endpoint is harmless
|
|
21
25
|
process.env.TOA_TELEMETRY_TRACES ??= JSON.stringify({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { Agent, request } from 'undici'
|
|
3
|
+
import { after, binding, then, when } from 'cucumber-tsflow'
|
|
4
|
+
import { PATH, PROBE } from '../../source/HTTP'
|
|
5
|
+
|
|
6
|
+
@binding()
|
|
7
|
+
export class Probe {
|
|
8
|
+
private readonly agent = new Agent()
|
|
9
|
+
private status: number | null = null
|
|
10
|
+
private headers: Record<string, string> = {}
|
|
11
|
+
|
|
12
|
+
@when('the ready probe is requested')
|
|
13
|
+
public async request (): Promise<void> {
|
|
14
|
+
const response = await request(`http://127.0.0.1:${PROBE}${PATH}`, { dispatcher: this.agent })
|
|
15
|
+
|
|
16
|
+
this.status = response.statusCode
|
|
17
|
+
this.headers = response.headers as Record<string, string>
|
|
18
|
+
|
|
19
|
+
await response.body.dump()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@then('the ready probe answers {int}')
|
|
23
|
+
public answers (status: number): void {
|
|
24
|
+
assert.equal(this.status, status)
|
|
25
|
+
|
|
26
|
+
if (status === 200)
|
|
27
|
+
assert.equal(this.headers['cache-control'], 'no-store')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@after()
|
|
31
|
+
public async close (): Promise<void> {
|
|
32
|
+
await this.agent.close()
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.276",
|
|
4
4
|
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@simplewebauthn/server": "13.3.3",
|
|
21
21
|
"@toa.io/core": "1.0.0-alpha.273",
|
|
22
22
|
"@toa.io/generic": "1.0.0-alpha.273",
|
|
23
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
23
|
+
"@toa.io/schemas": "1.0.0-alpha.274",
|
|
24
24
|
"bcryptjs": "3.0.3",
|
|
25
25
|
"content-type": "2.1.0",
|
|
26
26
|
"jose": "6.2.10",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"transpile:passkeys": "tsc -p ./components/identity.passkeys",
|
|
47
47
|
"transpile:otp": "tsc -p ./components/identity.otp",
|
|
48
48
|
"features": "cucumber-js",
|
|
49
|
+
"features:h2c": "TOA_EXPOSITION_PROTOCOL=h2c TOA_AGENT_PROTOCOL=h2c cucumber-js",
|
|
49
50
|
"features:security": "cucumber-js --tags @security",
|
|
50
51
|
"features:octets": "cucumber-js features/octets.*"
|
|
51
52
|
},
|
|
@@ -61,5 +62,5 @@
|
|
|
61
62
|
},
|
|
62
63
|
"testEnvironment": "node"
|
|
63
64
|
},
|
|
64
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "6f81af829c7bf8a2db5a409f3d60ea40d15583ef"
|
|
65
66
|
}
|
package/readme.md
CHANGED
|
@@ -188,21 +188,15 @@ exposition:
|
|
|
188
188
|
|
|
189
189
|
## Ready probe
|
|
190
190
|
|
|
191
|
-
The gateway serves `GET /.ready` on port `
|
|
191
|
+
The gateway serves `GET /.ready` on port `8004`, not on the port it serves traffic on, and
|
|
192
|
+
telemetry's probe is disabled in its process.
|
|
192
193
|
|
|
193
|
-
It
|
|
194
|
+
It answers `200` only after the in-process identity composition has connected, initial route
|
|
195
|
+
discovery has settled, and the HTTP server is listening. Before that it answers `503` with a
|
|
196
|
+
`retry-after`. When ready, the process also sends `process.send('ready')` for PM2 `wait_ready`.
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
3. The HTTP server is listening
|
|
198
|
-
|
|
199
|
-
Before listen, probes get connection refused; after listen but before ready, `503`.
|
|
200
|
-
When ready, the process also sends `process.send('ready')` (for PM2 `wait_ready`).
|
|
201
|
-
|
|
202
|
-
The nested identity composition shares this process. Deployment disables telemetry's port-`8001`
|
|
203
|
-
probe (`TOA_TELEMETRY_READY=false`) so only the gateway `/.ready` is used.
|
|
204
|
-
|
|
205
|
-
See also [telemetry ready probe](../telemetry/readme.md#ready-probe).
|
|
198
|
+
See also [telemetry ready probe](../telemetry/readme.md#ready-probe) and
|
|
199
|
+
[reserved ports](../../documentation/ports.md).
|
|
206
200
|
|
|
207
201
|
## See Also
|
|
208
202
|
|
|
@@ -24,8 +24,19 @@ properties:
|
|
|
24
24
|
patternProperties:
|
|
25
25
|
^.+$:
|
|
26
26
|
type: string
|
|
27
|
+
service:
|
|
28
|
+
type: object
|
|
29
|
+
properties:
|
|
30
|
+
annotations:
|
|
31
|
+
type: object
|
|
32
|
+
patternProperties:
|
|
33
|
+
^.+$:
|
|
34
|
+
type: string
|
|
27
35
|
debug:
|
|
28
36
|
type: boolean
|
|
37
|
+
protocol:
|
|
38
|
+
type: string
|
|
39
|
+
enum: [h1, h2c]
|
|
29
40
|
/: {}
|
|
30
41
|
authorities:
|
|
31
42
|
type: object
|
package/source/Annotation.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { Resources } from '@toa.io/operations'
|
|
2
|
+
import type { Protocol } from './HTTP'
|
|
2
3
|
|
|
3
4
|
export interface Annotation {
|
|
4
5
|
authorities: Record<string, string>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `h2c` requires an ingress controller that proxies cleartext HTTP/2 upstream.
|
|
9
|
+
* See `documentation/protocol.md`.
|
|
10
|
+
*/
|
|
11
|
+
protocol?: Protocol
|
|
5
12
|
class?: string
|
|
6
13
|
resources?: Resources
|
|
7
14
|
annotations?: Record<string, string>
|
|
15
|
+
|
|
16
|
+
/** The Service, as opposed to the Ingress that `annotations` above describes. */
|
|
17
|
+
service?: { annotations?: Record<string, string> }
|
|
18
|
+
|
|
8
19
|
debug?: boolean
|
|
9
20
|
'/'?: object // parsed and validated by RTD.syntax.parse
|
|
10
21
|
}
|
package/source/HTTP/Context.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Timing } from './Timing'
|
|
|
6
6
|
import { type Format, formats, types } from './formats'
|
|
7
7
|
import { read } from './messages'
|
|
8
8
|
import type { OutgoingMessage } from './messages'
|
|
9
|
-
import type
|
|
9
|
+
import type { IncomingMessage } from './types'
|
|
10
10
|
|
|
11
11
|
export class Context {
|
|
12
12
|
public readonly id: string
|
|
@@ -87,11 +87,6 @@ export class Context {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export interface IncomingMessage extends http.IncomingMessage {
|
|
91
|
-
url: string
|
|
92
|
-
method: string
|
|
93
|
-
}
|
|
94
|
-
|
|
95
90
|
interface Pipelines {
|
|
96
91
|
body: Array<(input: unknown) => unknown>
|
|
97
92
|
response: Array<(output: OutgoingMessage) => void | Promise<void>>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as http from 'node:http'
|
|
2
|
+
import { console } from 'openspan'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The readiness probe, on a port of its own.
|
|
6
|
+
*
|
|
7
|
+
* It cannot share the port the gateway serves traffic on: kubelet speaks HTTP/1.1 only, and a
|
|
8
|
+
* cleartext HTTP/2 server answers nothing else on its port — there is no ALPN to negotiate with.
|
|
9
|
+
*
|
|
10
|
+
* It is the gateway's own readiness, not the process's. The identity composition nested in this
|
|
11
|
+
* process connects before route discovery has settled, so a probe tied to that composition would
|
|
12
|
+
* report ready while the gateway still had no routes and no open port.
|
|
13
|
+
*/
|
|
14
|
+
export class Probe {
|
|
15
|
+
private readonly server = http.createServer()
|
|
16
|
+
private readonly port: number
|
|
17
|
+
private readonly path: string
|
|
18
|
+
|
|
19
|
+
private ready = false
|
|
20
|
+
private startedAt = 0
|
|
21
|
+
private listening = false
|
|
22
|
+
private skipped = false
|
|
23
|
+
|
|
24
|
+
public constructor (port: number, path: string = PATH) {
|
|
25
|
+
this.port = port
|
|
26
|
+
this.path = path
|
|
27
|
+
|
|
28
|
+
this.server.on('request', (request, response) => this.listener(request, response))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async listen (): Promise<void> {
|
|
32
|
+
this.startedAt = Date.now()
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await new Promise<void>((resolve, reject) => {
|
|
36
|
+
this.server.once('error', reject)
|
|
37
|
+
this.server.listen(this.port, () => {
|
|
38
|
+
this.server.removeListener('error', reject)
|
|
39
|
+
resolve()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
// processes sharing a host share the port; in a pod nothing else claims it
|
|
44
|
+
if (error?.code === 'EADDRINUSE') {
|
|
45
|
+
this.skipped = true
|
|
46
|
+
|
|
47
|
+
console.warn('Ready probe port already in use, skipping', { port: this.port })
|
|
48
|
+
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.listening = true
|
|
56
|
+
|
|
57
|
+
// a probe answers while the process runs; it must never be the reason it keeps running
|
|
58
|
+
this.server.unref()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The gateway is listening and has routes: answer `200`. */
|
|
62
|
+
public complete (): void {
|
|
63
|
+
this.ready = true
|
|
64
|
+
|
|
65
|
+
// the IPC signal is not tied to the probe: a process that gave up the shared port is
|
|
66
|
+
// still ready, and pm2 `wait_ready` would otherwise block until `listen_timeout`
|
|
67
|
+
process.send?.('ready')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async close (): Promise<void> {
|
|
71
|
+
this.ready = false
|
|
72
|
+
|
|
73
|
+
if (!this.listening || this.skipped)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
this.listening = false
|
|
77
|
+
|
|
78
|
+
// keep-alive connections would otherwise hold the server handle, delaying the exit
|
|
79
|
+
this.server.closeAllConnections()
|
|
80
|
+
|
|
81
|
+
await new Promise<void>((resolve) => this.server.close(() => resolve()))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private listener (request: http.IncomingMessage, response: http.ServerResponse): void {
|
|
85
|
+
if (request.url !== this.path) {
|
|
86
|
+
response.writeHead(404).end()
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.ready)
|
|
92
|
+
response.writeHead(200, { 'cache-control': 'no-store' }).end()
|
|
93
|
+
else {
|
|
94
|
+
const remaining = Math.ceil((Date.now() - this.startedAt) / 1000).toString()
|
|
95
|
+
|
|
96
|
+
response.writeHead(503, { 'retry-after': remaining }).end()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reserved for this probe. `8001` is the Telemetry readiness probe's, and `toa export` refuses a
|
|
103
|
+
* port claimed twice — `toa mono` and a local run put every service in one process.
|
|
104
|
+
*/
|
|
105
|
+
export const PROBE = 8004
|
|
106
|
+
export const PATH = '/.ready'
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The initial delay of the readiness probe. The server does not sleep for it: whoever
|
|
110
|
+
* probes is the one that waits, and doing it here as well only delayed the process twice.
|
|
111
|
+
*/
|
|
112
|
+
export const DELAY = 3 // seconds
|