@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 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.1.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.1.4"
19
+ "@yamf/test": "0.9.0"
13
20
  },
14
21
  "license": "MIT",
15
22
  "dependencies": {
16
- "@yamf/core": "0.8.1"
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` tallies.
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
- if (row?.node && load.has(row.node)) {
20
- load.set(row.node, (load.get(row.node) || 0) + 1)
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
- * @param {object} registry - server from `registryServer()`; must expose `registerCommand`, `getReplicasFor`, optional `_bundleStore`
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 - own URL (used for `registerCommand` cleanup key; often `YAMF_REGISTRY_URL`)
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 attachDeployRouter (registry, { bundleStore, location, pm3ServiceName = 'pm3-service' } = {}) {
53
+ export function registerDeployRouter (registry, { bundleStore, location, pm3ServiceName = 'pm3' } = {}) {
20
54
  if (!location) {
21
- throw new Error('attachDeployRouter: `location` (registry public URL) is required')
55
+ throw new Error('registerDeployRouter: `location` (registry public URL) is required')
22
56
  }
23
57
  if (!bundleStore && !registry?._bundleStore) {
24
- throw new Error('attachDeployRouter: pass bundleStore or start registry with a bundle store')
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 + planAndApply
29
- // client-side for parity; keep this for HTTP API consumers and future "registry as source of truth" flows.
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
- 'deploy-plan',
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 fromH = (reps || []).map((r) => r.sourceHash).filter(Boolean).join(',') || null
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
- /* no pub */
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
- 'deploy-bundle',
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) {