@yamf/services-deploy-router 0.1.0 → 0.9.0
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/LICENSE +21 -0
- package/package.json +12 -4
- package/placement.js +5 -4
- package/service.js +56 -19
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 mcbrumagin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yamf/services-deploy-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.0.0"
|
|
7
|
+
},
|
|
5
8
|
"description": "Registry plugin: deploy-plan + deploy-bundle (slice C3)",
|
|
6
9
|
"main": "service.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./service.js",
|
|
12
|
+
"./placement": "./placement.js"
|
|
13
|
+
},
|
|
7
14
|
"files": [
|
|
8
15
|
"service.js",
|
|
9
16
|
"placement.js"
|
|
10
17
|
],
|
|
11
18
|
"devDependencies": {
|
|
12
|
-
"@yamf/test": "0.
|
|
19
|
+
"@yamf/test": "0.9.0"
|
|
13
20
|
},
|
|
14
21
|
"license": "MIT",
|
|
15
22
|
"dependencies": {
|
|
16
|
-
"@yamf/core": "0.
|
|
23
|
+
"@yamf/core": "0.9.0"
|
|
17
24
|
},
|
|
18
25
|
"repository": {
|
|
19
26
|
"type": "git",
|
|
20
27
|
"url": "https://github.com/mcbrumagin/yamf"
|
|
21
28
|
},
|
|
22
29
|
"scripts": {
|
|
23
|
-
"test": "yamf test -d ."
|
|
30
|
+
"test": "yamf test -d .",
|
|
31
|
+
"test:all": "yamf test -d . --include-e2e"
|
|
24
32
|
}
|
|
25
33
|
}
|
package/placement.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HttpError } from '@yamf/core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* C4: least-loaded pm3 node by `replicaMetadata.node`
|
|
5
|
-
* @param {{ _state?: { replicaMetadata: Map<unknown, { node?: string }> }, listHealthyLocations?: (name: string) => string[] }} registry
|
|
4
|
+
* C4: least-loaded pm3 node by `replicaMetadata.nodeId` tallies (legacy `node` still counted if present).
|
|
5
|
+
* @param {{ _state?: { replicaMetadata: Map<unknown, { nodeId?: string, node?: string }> }, listHealthyLocations?: (name: string) => string[] }} registry
|
|
6
6
|
* @param {string} pm3ServiceName
|
|
7
7
|
* @param {{ excludeNodes?: string[] }} [opts]
|
|
8
8
|
*/
|
|
@@ -16,8 +16,9 @@ export function pickNode (registry, pm3ServiceName, { excludeNodes = [] } = {})
|
|
|
16
16
|
if (meta) {
|
|
17
17
|
const load = new Map(nodes.map((n) => [n, 0]))
|
|
18
18
|
for (const [, row] of meta) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const nid = row?.nodeId ?? row?.node
|
|
20
|
+
if (nid && load.has(nid)) {
|
|
21
|
+
load.set(nid, (load.get(nid) || 0) + 1)
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
return [...load.entries()].sort((a, b) => a[1] - b[1])[0][0]
|
package/service.js
CHANGED
|
@@ -9,26 +9,61 @@ import {
|
|
|
9
9
|
import { existsSync } from 'node:fs'
|
|
10
10
|
import { pickNode } from './placement.js'
|
|
11
11
|
|
|
12
|
+
const DEPLOY_HISTORY_MAX = 20
|
|
13
|
+
|
|
14
|
+
function pushDeployEvent (registry, event) {
|
|
15
|
+
const h = registry?._state?.deployHistory
|
|
16
|
+
if (!Array.isArray(h)) return
|
|
17
|
+
h.push(event)
|
|
18
|
+
if (h.length > DEPLOY_HISTORY_MAX) h.splice(0, h.length - DEPLOY_HISTORY_MAX)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wire verbs registered by {@link registerDeployRouter}. Exported so CLI / deploy-driver code
|
|
23
|
+
* can target them without stringly-typed literals.
|
|
24
|
+
*/
|
|
25
|
+
export const DEPLOY_COMMANDS = Object.freeze({
|
|
26
|
+
/** Server-side rollout decision; takes `{ services: [{ name, hash, replicas? }] }`. */
|
|
27
|
+
PLAN: 'deploy-plan',
|
|
28
|
+
/** Streamed bundle upload; body is the raw bundle, `yamf-deploy-hash` header required. */
|
|
29
|
+
BUNDLE: 'deploy-bundle'
|
|
30
|
+
})
|
|
31
|
+
|
|
12
32
|
const PLUGIN_SERVICE = 'yamf-deploy-router'
|
|
13
33
|
|
|
14
34
|
/**
|
|
15
|
-
*
|
|
35
|
+
* Install the deploy-router plugin onto a running registry.
|
|
36
|
+
*
|
|
37
|
+
* This is a **registry plugin** in the privileged in-process tier (see
|
|
38
|
+
* `@yamf/core/registry/command-router.js#registerCommand`): handlers run with direct
|
|
39
|
+
* `getReplicasFor` / `_bundleStore` access *after* token validation. It is **not** a
|
|
40
|
+
* service factory and is reserved for trusted boot code (CLI dev bootstrap, integration
|
|
41
|
+
* harnesses). For app-level extension via custom `yamf-command` verbs, see the v1 plan —
|
|
42
|
+
* service-extended commands are deferred to post-v1.
|
|
43
|
+
*
|
|
44
|
+
* Verbs registered: {@link DEPLOY_COMMANDS.PLAN} and {@link DEPLOY_COMMANDS.BUNDLE}.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} registry - server from `registryServer()`; must expose `registerCommand`,
|
|
47
|
+
* `getReplicasFor`, and (if `bundleStore` is omitted) `_bundleStore`.
|
|
16
48
|
* @param {{ bundleStore?: object, location: string, pm3ServiceName?: string }} options
|
|
17
|
-
* @param {string} options.location -
|
|
49
|
+
* @param {string} options.location - Own URL (used as the `registerCommand` cleanup key;
|
|
50
|
+
* typically `YAMF_REGISTRY_URL`).
|
|
51
|
+
* @returns {{ pickNode: (opts?: object) => string }} Helper bound to the registry / pm3 service name.
|
|
18
52
|
*/
|
|
19
|
-
export function
|
|
53
|
+
export function registerDeployRouter (registry, { bundleStore, location, pm3ServiceName = 'pm3' } = {}) {
|
|
20
54
|
if (!location) {
|
|
21
|
-
throw new Error('
|
|
55
|
+
throw new Error('registerDeployRouter: `location` (registry public URL) is required')
|
|
22
56
|
}
|
|
23
57
|
if (!bundleStore && !registry?._bundleStore) {
|
|
24
|
-
throw new Error('
|
|
58
|
+
throw new Error('registerDeployRouter: pass bundleStore or start registry with a bundle store')
|
|
25
59
|
}
|
|
26
60
|
const store = bundleStore || registry._bundleStore
|
|
27
61
|
|
|
28
|
-
// Server-side plan (auth: deploy token). The `yamf deploy` CLI still uses REGISTRY_PULL +
|
|
29
|
-
// client-side for parity;
|
|
62
|
+
// Server-side plan (auth: deploy token). The `yamf deploy` CLI still uses REGISTRY_PULL +
|
|
63
|
+
// planAndApply client-side for parity; this stays for HTTP API consumers and future
|
|
64
|
+
// "registry as source of truth" flows.
|
|
30
65
|
registry.registerCommand(
|
|
31
|
-
|
|
66
|
+
DEPLOY_COMMANDS.PLAN,
|
|
32
67
|
async ({ body, headers }) => {
|
|
33
68
|
const out = { decisions: [] }
|
|
34
69
|
for (const s of body?.services || []) {
|
|
@@ -38,18 +73,20 @@ export function attachDeployRouter (registry, { bundleStore, location, pm3Servic
|
|
|
38
73
|
const reps = registry.getReplicasFor(s.name) || []
|
|
39
74
|
const { decision } = deployDecisionFromReplicas(reps, s.hash, s.replicas ?? 1)
|
|
40
75
|
out.decisions.push({ service: s.name, hash: s.hash, decision })
|
|
41
|
-
const
|
|
76
|
+
const fromHash = reps.map((r) => r.sourceHash).filter(Boolean).join(',') || null
|
|
77
|
+
const deployEvent = {
|
|
78
|
+
service: s.name,
|
|
79
|
+
fromHash,
|
|
80
|
+
toHash: s.hash,
|
|
81
|
+
decision,
|
|
82
|
+
at: Date.now(),
|
|
83
|
+
deployer: headers[HEADERS.DEPLOYER] || null
|
|
84
|
+
}
|
|
85
|
+
pushDeployEvent(registry, deployEvent)
|
|
42
86
|
try {
|
|
43
|
-
await publishMessage('yamf:deploy',
|
|
44
|
-
service: s.name,
|
|
45
|
-
fromHash: fromH,
|
|
46
|
-
toHash: s.hash,
|
|
47
|
-
decision,
|
|
48
|
-
at: Date.now(),
|
|
49
|
-
deployer: headers[HEADERS.DEPLOYER] || null
|
|
50
|
-
})
|
|
87
|
+
await publishMessage('yamf:deploy', deployEvent)
|
|
51
88
|
} catch {
|
|
52
|
-
|
|
89
|
+
// best-effort observability; missing pubsub does not fail the plan
|
|
53
90
|
}
|
|
54
91
|
}
|
|
55
92
|
return out
|
|
@@ -64,7 +101,7 @@ export function attachDeployRouter (registry, { bundleStore, location, pm3Servic
|
|
|
64
101
|
)
|
|
65
102
|
|
|
66
103
|
registry.registerCommand(
|
|
67
|
-
|
|
104
|
+
DEPLOY_COMMANDS.BUNDLE,
|
|
68
105
|
async ({ request, headers }) => {
|
|
69
106
|
const hash = (headers[HEADERS.DEPLOY_HASH] || headers['yamf-deploy-hash'] || '').trim()
|
|
70
107
|
if (!hash) {
|