@homeflare/alchemy 0.54.0 → 0.55.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.
Files changed (43) hide show
  1. package/dist/ceph/ceph-argv-config.d.ts +6 -3
  2. package/dist/ceph/ceph-argv-config.d.ts.map +1 -1
  3. package/dist/ceph/ceph-argv.d.ts +8 -0
  4. package/dist/ceph/ceph-argv.d.ts.map +1 -1
  5. package/dist/ceph/ceph-config-option-form.d.ts +48 -0
  6. package/dist/ceph/ceph-config-option-form.d.ts.map +1 -0
  7. package/dist/ceph/ceph-config-option-reconcile.d.ts +22 -0
  8. package/dist/ceph/ceph-config-option-reconcile.d.ts.map +1 -0
  9. package/dist/ceph/ceph-config-options.d.ts +15 -6
  10. package/dist/ceph/ceph-config-options.d.ts.map +1 -1
  11. package/dist/ceph/config-option.d.ts +10 -0
  12. package/dist/ceph/config-option.d.ts.map +1 -0
  13. package/dist/ceph/index.d.ts +7 -3
  14. package/dist/ceph/index.d.ts.map +1 -1
  15. package/dist/ceph/index.js +363 -8
  16. package/dist/ceph/index.js.map +17 -7
  17. package/dist/linux/index.js +33 -2
  18. package/dist/linux/index.js.map +3 -3
  19. package/dist/linux/sudo-runner.d.ts +12 -0
  20. package/dist/linux/sudo-runner.d.ts.map +1 -1
  21. package/dist/proxmox/index.d.ts +1 -0
  22. package/dist/proxmox/index.d.ts.map +1 -1
  23. package/dist/proxmox/index.js +101 -1
  24. package/dist/proxmox/index.js.map +5 -4
  25. package/dist/proxmox/scoped-api.d.ts +49 -0
  26. package/dist/proxmox/scoped-api.d.ts.map +1 -0
  27. package/docs/pve-scoped-api.md +23 -0
  28. package/package.json +1 -1
  29. package/src/ceph/ceph-argv-config.ts +27 -19
  30. package/src/ceph/ceph-argv.test.ts +37 -1
  31. package/src/ceph/ceph-argv.ts +25 -0
  32. package/src/ceph/ceph-config-option-form.ts +73 -0
  33. package/src/ceph/ceph-config-option-reconcile.ts +88 -0
  34. package/src/ceph/ceph-config-option.test.ts +213 -0
  35. package/src/ceph/ceph-config-options.test.ts +28 -11
  36. package/src/ceph/ceph-config-options.ts +29 -8
  37. package/src/ceph/config-option.ts +112 -0
  38. package/src/ceph/index.ts +7 -3
  39. package/src/linux/sudo-runner-privileged-read.test.ts +92 -0
  40. package/src/linux/sudo-runner.ts +54 -1
  41. package/src/proxmox/index.ts +13 -0
  42. package/src/proxmox/scoped-api.test.ts +52 -0
  43. package/src/proxmox/scoped-api.ts +148 -0
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * `undefined` when `value` is a safe ceph config value — exported (like `boundedEntityProblem`)
3
- * so the shape itself is directly testable, since `NAMED_CONFIG_OPTIONS` being empty today means
4
- * `cephCommandProblem` never reaches this check on any real argv (`namedOptionProblem` refuses
5
- * first) — this stays exercised until K-D's first named option arms the path for real.
3
+ * so the shape itself is directly testable independently of whichever options are named.
6
4
  */
7
5
  export declare const configValueProblem: (value: string) => string | undefined;
6
+ /**
7
+ * `undefined` when `name` may be declared for `who`, adopted, read by value or written — see
8
+ * ceph-config-options.ts's header for why the list starts empty and stays reviewed line by line.
9
+ */
10
+ export declare const namedOptionProblem: (who: string, name: string) => string | undefined;
8
11
  /** `config get|set|rm <who> <name> [value]`, called with the operands after `config` stripped. */
9
12
  export declare const configProblem: (rest: readonly string[]) => string | undefined;
10
13
  //# sourceMappingURL=ceph-argv-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ceph-argv-config.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv-config.ts"],"names":[],"mappings":"AAiCA;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,UAAW,MAAM,KAAG,MAAM,GAAG,SAG0B,CAAC;AAevF,kGAAkG;AAClG,eAAO,MAAM,aAAa,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAmBhE,CAAC"}
1
+ {"version":3,"file":"ceph-argv-config.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv-config.ts"],"names":[],"mappings":"AAwCA;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAAW,MAAM,KAAG,MAAM,GAAG,SAG0B,CAAC;AAEvF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAS,MAAM,QAAQ,MAAM,KAAG,MAAM,GAAG,SAUvE,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,aAAa,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAmBhE,CAAC"}
@@ -16,4 +16,12 @@ export declare const authGetArgv: (entity: string) => readonly string[];
16
16
  export declare const authGetOrCreateArgv: (entity: string, caps: Readonly<Record<(typeof DAEMONS)[number], string>>) => readonly string[];
17
17
  export declare const authCapsArgv: (entity: string, caps: Readonly<Record<(typeof DAEMONS)[number], string>>) => readonly string[];
18
18
  export declare const quorumStatusArgv: () => readonly string[];
19
+ /**
20
+ * `config set <who> <name> <value>` — `Ceph.ConfigOption`'s write. ⚠️ The VALUE is in argv, so it
21
+ * lands in the runner's log and the node's sudo journal; acceptable only because the named option
22
+ * list carries no secret-valued option (ceph-config-options.ts).
23
+ */
24
+ export declare const configSetArgv: (who: string, name: string, value: string) => readonly string[];
25
+ /** `config rm <who> <name>` — `Ceph.ConfigOption`'s delete; idempotent on the ceph side. */
26
+ export declare const configRmArgv: (who: string, name: string) => readonly string[];
19
27
  //# sourceMappingURL=ceph-argv.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ceph-argv.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC,mEAAmE;AACnE,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,yFAAyF;AACzF,eAAO,MAAM,oBAAoB,WAAY,MAAM,KAAG,MAAM,GAAG,SAGiD,CAAC;AAejH,QAAA,MAAM,OAAO,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AA6E/C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAWrE,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,WAAW,WAAY,MAAM,KAAG,SAAS,MAAM,EAM3D,CAAC;AAEF,eAAO,MAAM,mBAAmB,WACtB,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,YAAY,WACf,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,SAAS,MAAM,EAIlD,CAAC"}
1
+ {"version":3,"file":"ceph-argv.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC,mEAAmE;AACnE,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,yFAAyF;AACzF,eAAO,MAAM,oBAAoB,WAAY,MAAM,KAAG,MAAM,GAAG,SAGiD,CAAC;AAejH,QAAA,MAAM,OAAO,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AA6E/C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAWrE,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,WAAW,WAAY,MAAM,KAAG,SAAS,MAAM,EAM3D,CAAC;AAEF,eAAO,MAAM,mBAAmB,WACtB,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,YAAY,WACf,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,SAAS,MAAM,EAIlD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,QAAQ,MAAM,SAAS,MAAM,KAAG,SAAS,MAAM,EAQvF,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,YAAY,QAAS,MAAM,QAAQ,MAAM,KAAG,SAAS,MAAM,EAOvE,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Props, attributes and the pure helpers for `Ceph.ConfigOption` (K-D) — split from
3
+ * config-option.ts the same way ceph-auth-form.ts is split from auth-entity.ts: nothing here
4
+ * talks to a cluster, so every rule is unit-tested without PVE or ssh.
5
+ * Design: docs/plans/2026-09-26-ceph-config-options.md.
6
+ *
7
+ * ⛔ VALIDATION IS THE TRANSPORT'S OWN ALLOWLIST, NOT A SECOND COPY OF IT. `configOptionProblem`
8
+ * builds the exact `config set` argv the write would run and asks `cephCommandProblem` — so the
9
+ * named list (ceph-config-options.ts), the per-option `<who>` bound, the lockout classes and the
10
+ * flag-shaped-value refusal can never disagree between plan time and write time.
11
+ */
12
+ import type { ListNodeCephCfgDbResponseBodyItem } from '@distilled.cloud/proxmox/nodes';
13
+ import type { WithTarget } from '../proxmox/resource-spec.ts';
14
+ export interface CephConfigOptionProps extends WithTarget {
15
+ /**
16
+ * The PVE node whose `GET /nodes/{node}/ceph/cfg/db` answers the mon config db — the plan-time
17
+ * read. Any node of the Ceph cluster answers the same db.
18
+ */
19
+ node: string;
20
+ /** ssh destinations of the mon hosts the write (`config set`/`rm`) is tried against, in order. */
21
+ monNodes: readonly string[];
22
+ /** The config section: `global`, `mon`, `osd`, `mgr`, `mds`, `client`, optionally `.<id>`. */
23
+ who: string;
24
+ /** The option name — must be on `NAMED_CONFIG_OPTIONS` for exactly this `who`. */
25
+ name: string;
26
+ /**
27
+ * The value as ceph stores it — a string, compared byte-for-byte with what the config db reports.
28
+ * ⚠️ Spell it the way ceph echoes it back (`false`, not `0`), or every plan is an `update`.
29
+ */
30
+ value: string;
31
+ }
32
+ export interface CephConfigOptionAttributes {
33
+ who: string;
34
+ name: string;
35
+ /** The mon-db value observed after the last reconcile. The only field `matches` compares. */
36
+ value: string;
37
+ }
38
+ /** One row of the mon config db, as the PVE API reports it (`ceph config dump`'s shape). */
39
+ export type CfgDbRow = Pick<ListNodeCephCfgDbResponseBodyItem, 'section' | 'name' | 'value' | 'mask'>;
40
+ /** `undefined` when props may be declared at all; else why not. Runs before any read or write. */
41
+ export declare const configOptionProblem: (props: CephConfigOptionProps) => string | undefined;
42
+ /**
43
+ * The live row for exactly `{who, name}` with NO mask. ⚠️ A masked row (`osd/class:ssd`) is a
44
+ * different setting the bare `config set <who>` neither writes nor removes, so it is never matched.
45
+ */
46
+ export declare const findLive: (rows: readonly CfgDbRow[], who: string, name: string) => CephConfigOptionAttributes | undefined;
47
+ export declare const matches: (live: CephConfigOptionAttributes | undefined, props: CephConfigOptionProps) => boolean;
48
+ //# sourceMappingURL=ceph-config-option-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ceph-config-option-form.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-option-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,MAAM,WAAW,qBAAsB,SAAQ,UAAU;IACvD;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,8FAA8F;IAC9F,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,KAAK,EAAE,MAAM,CAAC;CACf;AAED,4FAA4F;AAC5F,MAAM,MAAM,QAAQ,GAAG,IAAI,CACzB,iCAAiC,EACjC,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CACtC,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,mBAAmB,UAAW,qBAAqB,KAAG,MAAM,GAAG,SAM3E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,SACb,SAAS,QAAQ,EAAE,OACpB,MAAM,QACL,MAAM,KACX,0BAA0B,GAAG,SAG/B,CAAC;AAEF,eAAO,MAAM,OAAO,SACZ,0BAA0B,GAAG,SAAS,SACrC,qBAAqB,YACuB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The observe -> ensure -> sync loop and the delete for `Ceph.ConfigOption` (K-D) — plain
3
+ * functions, so a fake-runner test drives the real loop (same split as ceph-auth-reconcile.ts).
4
+ *
5
+ * ★ OBSERVE IS THE PVE API, WRITES ARE THE SSH MON TRANSPORT. The PVE API is read-only for the
6
+ * config db (`GET /nodes/{node}/ceph/cfg/db`; no write endpoint exists), so writes go through
7
+ * ceph-transport.ts's `runCephCommand` — the one transport every Ceph family in this kit uses,
8
+ * allowlist-checked before it dials — and each write is followed by `assertFreshQuorum`.
9
+ * ★ A DEPLOY WITH NO DRIFT MAKES ZERO WRITES (S10). If the observed mon-db value already equals
10
+ * props — an adopted, already-set option included — nothing reaches ssh at all.
11
+ * ⛔ A FAILED READ IS NEVER "ABSENT". The caller's `readRows` propagates its own failure; only a
12
+ * successful read with no matching row means the option is unset.
13
+ * ⛔ READ BACK AFTER EVERY WRITE and refuse to record a value the mon db does not report.
14
+ */
15
+ import * as Effect from 'effect/Effect';
16
+ import { type CephConfigOptionAttributes, type CephConfigOptionProps, type CfgDbRow } from './ceph-config-option-form.ts';
17
+ import { type CephTransportOptions } from './ceph-transport.ts';
18
+ export type CephConfigDeps = Pick<CephTransportOptions, 'dial' | 'log'>;
19
+ export declare const reconcileCephConfigOption: <E, R>(props: CephConfigOptionProps, readRows: Effect.Effect<readonly CfgDbRow[], E, R>, deps: CephConfigDeps) => Effect.Effect<CephConfigOptionAttributes, E | Error, R>;
20
+ /** Idempotent: an option already absent from the mon db is success, with nothing sent. */
21
+ export declare const deleteCephConfigOption: <E, R>(props: Pick<CephConfigOptionProps, 'who' | 'name' | 'monNodes'>, readRows: Effect.Effect<readonly CfgDbRow[], E, R>, deps: CephConfigDeps) => Effect.Effect<void, E | Error, R>;
22
+ //# sourceMappingURL=ceph-config-option-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ceph-config-option-reconcile.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-option-reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EAId,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,KAAK,oBAAoB,EAAqC,MAAM,qBAAqB,CAAC;AAEnG,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;AAsBxE,eAAO,MAAM,yBAAyB,GAAI,CAAC,EAAE,CAAC,SACrC,qBAAqB,YAClB,MAAM,CAAC,MAAM,CAAC,SAAS,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAC5C,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,0BAA0B,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAuBrD,CAAC;AAEL,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,GAAI,CAAC,EAAE,CAAC,SAClC,IAAI,CAAC,qBAAqB,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC,YACrD,MAAM,CAAC,MAAM,CAAC,SAAS,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAC5C,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAK/B,CAAC"}
@@ -3,10 +3,14 @@
3
3
  * through the mon-command transport (ceph-argv.ts's `configProblem`). Design:
4
4
  * docs/plans/2026-09-26-ceph-config-options.md, D-C1.
5
5
  *
6
- * ⛔ THE LIST STARTS EMPTY, AND EACH ADDITION IS ITS OWN REVIEWED PR LINE. Nothing in K-A4 adds to
7
- * it — `CephConfigOption` (K-D) is a separate, later family; this file exists now only so the
8
- * transport's allowlist has something to bound `config` against from day one, per the accepted
9
- * design ("config get/set for the named option list which starts EMPTY").
6
+ * ⛔ THE LIST STARTED EMPTY, AND EACH ADDITION IS ITS OWN REVIEWED PR LINE. `Ceph.ConfigOption`
7
+ * (K-D, config-option.ts) is the only declarer; it refuses any `{who, name}` this map does not
8
+ * carry before planning, and the transport's allowlist (ceph-argv-config.ts) refuses it again
9
+ * before anything dials. One entry per line, each with a comment saying what it does and why it
10
+ * is managed — never a batch.
11
+ * ★ EACH ENTRY ALSO NAMES THE `<who>` IT MAY BE SET FOR. This is the config-options design's
12
+ * acceptance test #1 ("`<who>` bounded to the declared daemons"): an option listed for `mgr`
13
+ * can never be written to `global`, `client.admin` or any other section through this transport.
10
14
  * ⛔ NEVER ADD ONE OF THE LOCKOUT CLASSES BELOW. `mon_host`, `public_network`, `cluster_network`,
11
15
  * `public_addr`, `cluster_addr`, every `auth_*_required` and every `key`/`keyring`-bearing
12
16
  * option re-address the mons or de-authenticate the whole cluster — cutting every VM disk — and
@@ -16,8 +20,13 @@
16
20
  * cluster to measure this against yet); a secret-bearing setting belongs in a vault-backed flow,
17
21
  * never a `ceph config set` argv, which lands in the runner's log and the node's sudo journal.
18
22
  */
19
- /** Starts empty. Add one reviewed option name per PR line, never a batch. */
20
- export declare const NAMED_CONFIG_OPTIONS: ReadonlySet<string>;
23
+ /**
24
+ * option name -> the `<who>` sections it may be set for. Add one reviewed line per PR, never a
25
+ * batch; the structural test (ceph-config-options.test.ts) runs the lockout guard over every key.
26
+ */
27
+ export declare const NAMED_CONFIG_OPTIONS: ReadonlyMap<string, readonly string[]>;
28
+ /** The `<who>` sections `name` may be set for — empty when the option is not on the list. */
29
+ export declare const allowedWhoFor: (name: string) => readonly string[];
21
30
  /** Whether `name` is a class this transport must never read a value for, adopt, or write. */
22
31
  export declare const isLockoutConfigOption: (name: string) => boolean;
23
32
  //# sourceMappingURL=ceph-config-options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ceph-config-options.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAAe,CAAC;AAarE,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,SAAU,MAAM,KAAG,OACE,CAAC"}
1
+ {"version":3,"file":"ceph-config-options.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAOtE,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,aAAa,SAAU,MAAM,KAAG,SAAS,MAAM,EACtB,CAAC;AAgBvC,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,SAAU,MAAM,KAAG,OACE,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Resource } from 'alchemy';
2
+ import * as Provider from 'alchemy/Provider';
3
+ import type { PveRequirements } from '../proxmox/resource-spec.ts';
4
+ import { type CephConfigOptionAttributes, type CephConfigOptionProps } from './ceph-config-option-form.ts';
5
+ export type { CephConfigOptionAttributes, CephConfigOptionProps, } from './ceph-config-option-form.ts';
6
+ export interface CephConfigOption extends Resource<'Ceph.ConfigOption', CephConfigOptionProps, CephConfigOptionAttributes, never, PveRequirements> {
7
+ }
8
+ export declare const CephConfigOption: import("alchemy").ResourceClass<CephConfigOption>;
9
+ export declare const CephConfigOptionProvider: () => import("effect/Layer").Layer<Provider.Provider<CephConfigOption>, never, import("effect/http/HttpClient").HttpClient>;
10
+ //# sourceMappingURL=config-option.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-option.d.ts","sourceRoot":"","sources":["../../src/ceph/config-option.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAI3B,MAAM,8BAA8B,CAAC;AAMtC,YAAY,EACV,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,8BAA8B,CAAC;AAEtC,MAAM,WAAW,gBAAiB,SAAQ,QAAQ,CAChD,mBAAmB,EACnB,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,gBAAgB,mDAE3B,CAAC;AAWH,eAAO,MAAM,wBAAwB,6HAsClC,CAAC"}
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Ceph providers for Alchemy — the mon-command transport and `Ceph.AuthEntity` (K-A4).
2
+ * Ceph providers for Alchemy — the mon-command transport, `Ceph.AuthEntity` (K-A4) and
3
+ * `Ceph.ConfigOption` (K-D).
3
4
  *
4
5
  * ⛔ THIS BARREL IS THE PUBLIC API, DELIBERATELY SMALLER THAN THE DIRECTORY — see
5
6
  * `../openbao/index.ts`'s header for why. The allowlist internals (ceph-argv.ts), the parsing
6
- * (ceph-auth-parse.ts) and the reconcile loop (ceph-auth-reconcile.ts) are exported by path for
7
- * a future `CephConfigOption` (K-D) or a test to reach directly, not published here.
7
+ * (ceph-auth-parse.ts) and the reconcile loops (ceph-auth-reconcile.ts,
8
+ * ceph-config-option-reconcile.ts) are reached by path from tests, not published here.
8
9
  */
9
10
  export type { CephAuthEntityAttributes, CephAuthEntityProps, CephCaps } from './auth-entity.ts';
10
11
  export { CephAuthEntity, CephAuthEntityProvider } from './auth-entity.ts';
11
12
  export type { CephDial, CephExecResult, CephTransportOptions } from './ceph-transport.ts';
12
13
  export { assertFreshQuorum, runCephCommand, sshCephDial } from './ceph-transport.ts';
14
+ export type { CephConfigOptionAttributes, CephConfigOptionProps } from './config-option.ts';
15
+ export { CephConfigOption, CephConfigOptionProvider } from './config-option.ts';
16
+ export { NAMED_CONFIG_OPTIONS } from './ceph-config-options.ts';
13
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ceph/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,YAAY,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAChG,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ceph/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,YAAY,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAChG,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACrF,YAAY,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -187,7 +187,10 @@ var canonical = (text) => text.trimEnd();
187
187
  var sha256 = (text) => new Bun.CryptoHasher("sha256").update(canonical(text)).digest("hex");
188
188
 
189
189
  // src/ceph/ceph-config-options.ts
190
- var NAMED_CONFIG_OPTIONS = new Set([]);
190
+ var NAMED_CONFIG_OPTIONS = new Map([
191
+ ["mgr/prometheus/exclude_perf_counters", ["mgr"]]
192
+ ]);
193
+ var allowedWhoFor = (name) => NAMED_CONFIG_OPTIONS.get(name) ?? [];
191
194
  var LOCKOUT_EXACT = new Set([
192
195
  "mon_host",
193
196
  "public_network",
@@ -195,20 +198,23 @@ var LOCKOUT_EXACT = new Set([
195
198
  "public_addr",
196
199
  "cluster_addr"
197
200
  ]);
198
- var LOCKOUT_PATTERN = /^auth_\w*_required$|(^|_)key(ring)?($|_)/;
201
+ var LOCKOUT_PATTERN = /^auth_\w*_required$|(^|[_/])key(ring)?($|[_/])/;
199
202
  var isLockoutConfigOption = (name) => LOCKOUT_EXACT.has(name) || LOCKOUT_PATTERN.test(name);
200
203
 
201
204
  // src/ceph/ceph-argv-config.ts
202
205
  var WHO = /^(global|mon|osd|mgr|mds|client)(\.[A-Za-z0-9_-]{1,32})?$/;
203
- var OPTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
206
+ var OPTION_NAME = /^(?:[a-z][a-z0-9_]{0,63}|mgr\/[a-z][a-z0-9_]{0,31}\/[a-z][a-z0-9_]{0,63})$/;
204
207
  var CONFIG_VALUE = /^(?:[A-Za-z0-9][A-Za-z0-9 ._:/=,-]{0,255}|-[0-9]{1,255})$/;
205
208
  var configValueProblem = (value) => CONFIG_VALUE.test(value) ? undefined : `config value ${JSON.stringify(value)} contains characters outside the safe set`;
206
- var namedOptionProblem = (name) => {
209
+ var namedOptionProblem = (who, name) => {
207
210
  if (!OPTION_NAME.test(name))
208
211
  return `${JSON.stringify(name)} is not a plain ceph option name`;
209
212
  if (isLockoutConfigOption(name))
210
213
  return `${name} is a lockout-class option and can never be declared`;
211
- return NAMED_CONFIG_OPTIONS.has(name) ? undefined : `${name} is not on the named option list (it starts empty — see ceph-config-options.ts)`;
214
+ if (!NAMED_CONFIG_OPTIONS.has(name))
215
+ return `${name} is not on the named option list (each entry is a reviewed PR line — see ceph-config-options.ts)`;
216
+ const allowed = allowedWhoFor(name);
217
+ return allowed.includes(who) ? undefined : `${name} may only be set for ${allowed.join("/")} on the named option list, not ${JSON.stringify(who)}`;
212
218
  };
213
219
  var configProblem = (rest) => {
214
220
  const [verb, who, name, ...extra] = rest;
@@ -218,13 +224,13 @@ var configProblem = (rest) => {
218
224
  if (name === undefined)
219
225
  return "config takes a `<name>` from the named option list";
220
226
  if (verb === "get" || verb === "rm") {
221
- return extra.length === 0 ? namedOptionProblem(name) : `config ${String(verb)} takes exactly \`<who> <name>\``;
227
+ return extra.length === 0 ? namedOptionProblem(who, name) : `config ${String(verb)} takes exactly \`<who> <name>\``;
222
228
  }
223
229
  if (verb === "set") {
224
230
  const [value, ...more] = extra;
225
231
  if (value === undefined || more.length !== 0)
226
232
  return "config set takes exactly `<who> <name> <value>`";
227
- const named = namedOptionProblem(name);
233
+ const named = namedOptionProblem(who, name);
228
234
  return named !== undefined ? named : configValueProblem(value);
229
235
  }
230
236
  return `ceph config ${String(verb)} is not on the allowlist`;
@@ -335,6 +341,23 @@ var quorumStatusArgv = () => [
335
341
  "quorum_status",
336
342
  ...JSON_FORMAT
337
343
  ];
344
+ var configSetArgv = (who, name, value) => [
345
+ CEPH_BIN,
346
+ "config",
347
+ "set",
348
+ who,
349
+ name,
350
+ value,
351
+ ...JSON_FORMAT
352
+ ];
353
+ var configRmArgv = (who, name) => [
354
+ CEPH_BIN,
355
+ "config",
356
+ "rm",
357
+ who,
358
+ name,
359
+ ...JSON_FORMAT
360
+ ];
338
361
 
339
362
  // src/ceph/ceph-auth-parse.ts
340
363
  var isRecord = (value) => typeof value === "object" && value !== null;
@@ -1598,13 +1621,345 @@ var CephAuthEntityProvider = () => Provider.effect(CephAuthEntity, Effect3.succe
1598
1621
  reconcile: ({ news, output }) => reconcileCephAuthEntity(news, output, {}),
1599
1622
  delete: ({ output }) => Effect3.die(refusedDelete(output.entity))
1600
1623
  })));
1624
+ // src/ceph/config-option.ts
1625
+ import * as nodes from "@distilled.cloud/proxmox/nodes";
1626
+ import { Resource as Resource2 } from "alchemy";
1627
+ import { Unowned } from "alchemy/AdoptPolicy";
1628
+ import { isResolved as isResolved2 } from "alchemy/Diff";
1629
+ import * as Provider2 from "alchemy/Provider";
1630
+ import * as Effect10 from "effect/Effect";
1631
+
1632
+ // src/proxmox/credentials.ts
1633
+ var VALID_TIER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1634
+ var mintTier = (target, role) => {
1635
+ if (target.scheme !== "pve")
1636
+ return role;
1637
+ const tier = target.roles?.[role];
1638
+ if (tier === undefined)
1639
+ return role;
1640
+ if (!VALID_TIER.test(tier)) {
1641
+ throw new Error(`PveTarget.roles.${role} = ${JSON.stringify(tier)} is not a valid OpenBao tier name -- ` + 'it would be interpolated straight into "<mount>/creds/<tier>" and must be a plain name ' + '(letters, digits, ".", "_", "-", not starting with one of those and not "..").');
1642
+ }
1643
+ return tier;
1644
+ };
1645
+
1646
+ // src/proxmox/distilled-pve.ts
1647
+ import * as Effect7 from "effect/Effect";
1648
+ import * as Result2 from "effect/Result";
1649
+ import { credentials as distilledPveCredentials } from "@distilled.cloud/proxmox/Credentials";
1650
+ import * as ProxmoxRetry from "@distilled.cloud/proxmox/Retry";
1651
+
1652
+ // src/proxmox/lease-cache.ts
1653
+ import * as Cache from "effect/Cache";
1654
+ import * as Duration from "effect/Duration";
1655
+ import * as Effect5 from "effect/Effect";
1656
+ import * as Exit from "effect/Exit";
1657
+
1658
+ // src/proxmox/mint.ts
1659
+ import * as Effect4 from "effect/Effect";
1660
+ import * as Option2 from "effect/Option";
1661
+ import * as FetchHttpClient2 from "effect/http/FetchHttpClient";
1662
+ import * as Headers2 from "effect/http/Headers";
1663
+ import * as HttpClient3 from "effect/http/HttpClient";
1664
+ import * as HttpClientRequest2 from "effect/http/HttpClientRequest";
1665
+
1666
+ // src/proxmox/credential-errors.ts
1667
+ import * as Data from "effect/Data";
1668
+
1669
+ class PveCredentialDenied extends Data.TaggedError("PveCredentialDenied") {
1670
+ get message() {
1671
+ return `OpenBao GET /v1/${this.mount}/creds/${this.tier} -> 403: ${this.detail}. This identity's ` + "AppRole has no grant for this role -- a refused read, not evidence the object is gone.";
1672
+ }
1673
+ }
1674
+
1675
+ // src/proxmox/mint.ts
1676
+ var baoVariable = (env, name) => env[`BAO_${name}`] ?? env[`VAULT_${name}`] ?? "";
1677
+ var mint = (target, role, env = process.env) => Effect4.gen(function* () {
1678
+ const tier = mintTier(target, role);
1679
+ const path = `${target.mount}/creds/${tier}`;
1680
+ const refuse = (status2, detail) => new Error(`OpenBao GET /v1/${path} -> ${status2 === 0 ? "no response" : status2}: ${detail}`);
1681
+ const address = baoVariable(env, "AGENT_ADDR") || baoVariable(env, "ADDR") || "https://127.0.0.1:8200";
1682
+ const socket = address.startsWith("unix://") ? address.slice("unix://".length) : undefined;
1683
+ const base = socket === undefined ? address.replace(/\/+$/, "") : "http://localhost";
1684
+ const headers = { "X-Vault-Request": "true" };
1685
+ const namespace = baoVariable(env, "NAMESPACE");
1686
+ if (namespace !== "")
1687
+ headers["X-Vault-Namespace"] = namespace;
1688
+ const token = baoVariable(env, "TOKEN");
1689
+ if (token !== "")
1690
+ headers["X-Vault-Token"] = token;
1691
+ if (socket !== undefined && !("Bun" in globalThis)) {
1692
+ return yield* Effect4.fail(refuse(0, "a unix:// address needs Bun fetch; this is not Bun"));
1693
+ }
1694
+ const client = yield* HttpClient3.HttpClient;
1695
+ const redacted = yield* Headers2.CurrentRedactedNames;
1696
+ const defaults = Option2.getOrElse(yield* Effect4.serviceOption(FetchHttpClient2.RequestInit), () => ({}));
1697
+ const request = HttpClientRequest2.get(`${base}/v1/${path}`).pipe(HttpClientRequest2.setHeaders(headers));
1698
+ const exchange = client.execute(request).pipe(Effect4.flatMap((response) => Effect4.map(response.text, (text2) => ({ status: response.status, text: text2 }))), Effect4.timeout("60 seconds"), Effect4.provideService(Headers2.CurrentRedactedNames, [...redacted, "x-vault-token"]));
1699
+ const init = { ...defaults, unix: socket };
1700
+ const { status, text } = yield* (socket === undefined ? exchange : Effect4.provideService(exchange, FetchHttpClient2.RequestInit, init)).pipe(Effect4.mapError((cause) => refuse(0, cause.message)));
1701
+ let parsed;
1702
+ try {
1703
+ parsed = JSON.parse(text);
1704
+ } catch {
1705
+ parsed = undefined;
1706
+ }
1707
+ if (status < 200 || status >= 300) {
1708
+ const errors = parsed?.errors;
1709
+ const isOpenBaoBody = Array.isArray(errors);
1710
+ const detail = isOpenBaoBody ? errors.map(String).join("; ") : "not an OpenBao error body";
1711
+ if (status === 403 && isOpenBaoBody) {
1712
+ return yield* Effect4.fail(new PveCredentialDenied({ detail, mount: target.mount, tier }));
1713
+ }
1714
+ return yield* Effect4.fail(refuse(status, detail || "(no errors given)"));
1715
+ }
1716
+ const tokenId = parsed?.data?.token_id;
1717
+ const secret = parsed?.data?.secret;
1718
+ if (typeof tokenId !== "string" || typeof secret !== "string") {
1719
+ return yield* Effect4.fail(new Error(`${target.mount}/creds/${tier} returned no token_id/secret. Either this approle lacks ` + `the grant, or the mount is not ${target.mount}. Say which mount and role you needed ` + `than falling back to a stored credential -- every stored PVE token is read-only.`));
1720
+ }
1721
+ const lease = parsed?.lease_duration;
1722
+ const leaseSeconds = typeof lease === "number" ? lease : 0;
1723
+ return { leaseSeconds, secret, tokenId };
1724
+ });
1725
+ var authorization = (credential, target) => target.scheme === "pbs" ? `PBSAPIToken=${credential.tokenId}:${credential.secret}` : `PVEAPIToken=${credential.tokenId}=${credential.secret}`;
1726
+
1727
+ // src/proxmox/lease-cache.ts
1728
+ var REMINT_MARGIN_SECONDS = 60;
1729
+ var keyOf = (target, role) => ({
1730
+ mount: target.mount,
1731
+ scheme: target.scheme,
1732
+ tier: mintTier(target, role)
1733
+ });
1734
+ var timeToLive = (exit) => Exit.isSuccess(exit) && exit.value.leaseSeconds > REMINT_MARGIN_SECONDS ? Duration.seconds(exit.value.leaseSeconds - REMINT_MARGIN_SECONDS) : Duration.zero;
1735
+ var makeLeases = (mintFor) => Cache.makeWith(mintFor, { capacity: 64, requireServicesAt: "lookup", timeToLive });
1736
+ var targetForKey = (key) => key.scheme === "pbs" ? { api: "https://pbs.invalid/api2/json", mount: key.mount, scheme: "pbs" } : { members: [], mount: key.mount, roles: { read: key.tier }, scheme: "pve" };
1737
+ var mintForKey = (key, env = process.env) => mint(targetForKey(key), key.scheme === "pbs" ? key.tier : "read", env);
1738
+ var leases = Effect5.runSync(makeLeases(mintForKey));
1739
+ var leased = (target, role) => Cache.get(leases, keyOf(target, role));
1740
+
1741
+ // src/proxmox/members.ts
1742
+ import * as Cause2 from "effect/Cause";
1743
+ import * as Effect6 from "effect/Effect";
1744
+ import * as Result from "effect/Result";
1745
+ import * as HttpClient5 from "effect/http/HttpClient";
1746
+ import * as HttpClientError from "effect/http/HttpClientError";
1747
+ var PVE_API_PORT = 8006;
1748
+ var pveApiBase = (member) => `https://${member}:${String(PVE_API_PORT)}/api2/json`;
1749
+ var MEMBER_TIMEOUT = "20 seconds";
1750
+ var lastGoodByMount = new Map;
1751
+ var noteGoodMember = (target, member) => {
1752
+ lastGoodByMount.set(target.mount, member);
1753
+ };
1754
+ var orderedMembers = (target) => {
1755
+ const preferred = lastGoodByMount.get(target.mount);
1756
+ if (preferred === undefined || !target.members.includes(preferred))
1757
+ return target.members;
1758
+ return [preferred, ...target.members.filter((member) => member !== preferred)];
1759
+ };
1760
+ var errorCode = (cause) => {
1761
+ if (cause !== null && typeof cause === "object") {
1762
+ if ("code" in cause && typeof cause.code === "string")
1763
+ return cause.code;
1764
+ if ("cause" in cause)
1765
+ return errorCode(cause.cause);
1766
+ }
1767
+ return;
1768
+ };
1769
+ var isTimeoutCause = (cause) => {
1770
+ const code = errorCode(cause);
1771
+ if (code === "AbortError" || code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
1772
+ return true;
1773
+ }
1774
+ return cause instanceof Error && /timed out|timeout/i.test(cause.message);
1775
+ };
1776
+ var PRE_SEND_CODES = new Set(["ConnectionRefused", "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]);
1777
+ var isCertificateCode = (code) => /^(ERR_TLS_CERT_|CERT_|UNABLE_TO_VERIFY_|DEPTH_ZERO_SELF_SIGNED|SELF_SIGNED_CERT)/.test(code);
1778
+ var isPreSendCause = (cause) => {
1779
+ const code = errorCode(cause);
1780
+ return code !== undefined && (PRE_SEND_CODES.has(code) || isCertificateCode(code));
1781
+ };
1782
+ var isTransportFailure = (cause) => {
1783
+ if (Cause2.isTimeoutError(cause))
1784
+ return true;
1785
+ if (!HttpClientError.isHttpClientError(cause))
1786
+ return false;
1787
+ const tag = cause.reason._tag;
1788
+ return tag === "TransportError" || tag === "InvalidUrlError" || tag === "EncodeError";
1789
+ };
1790
+ var isPreSendTransport = (cause) => {
1791
+ if (!HttpClientError.isHttpClientError(cause))
1792
+ return false;
1793
+ const { reason } = cause;
1794
+ if (reason._tag === "InvalidUrlError" || reason._tag === "EncodeError")
1795
+ return true;
1796
+ if (reason._tag === "TransportError") {
1797
+ if (isTimeoutCause(reason.cause))
1798
+ return false;
1799
+ return isPreSendCause(reason.cause);
1800
+ }
1801
+ return false;
1802
+ };
1803
+ var describeTransport = (cause) => {
1804
+ if (Cause2.isTimeoutError(cause))
1805
+ return `timed out after ${String(MEMBER_TIMEOUT)}`;
1806
+ if (HttpClientError.isHttpClientError(cause))
1807
+ return cause.message;
1808
+ return String(cause);
1809
+ };
1810
+ var executeOnCluster = (target, method, path, build, timeout4 = MEMBER_TIMEOUT) => Effect6.gen(function* () {
1811
+ const client = yield* HttpClient5.HttpClient;
1812
+ const isWrite = method !== "GET";
1813
+ const attempts = [];
1814
+ for (const member of orderedMembers(target)) {
1815
+ const outcome = yield* client.execute(build(pveApiBase(member))).pipe(Effect6.timeout(timeout4), Effect6.result);
1816
+ if (Result.isSuccess(outcome)) {
1817
+ noteGoodMember(target, member);
1818
+ return outcome.success;
1819
+ }
1820
+ const why = describeTransport(outcome.failure);
1821
+ attempts.push({ member, why });
1822
+ const mayRetry = isWrite ? isPreSendTransport(outcome.failure) : isTransportFailure(outcome.failure);
1823
+ if (!mayRetry) {
1824
+ return yield* Effect6.fail({ attempts, method, path, tag: "cluster" });
1825
+ }
1826
+ }
1827
+ return yield* Effect6.fail({ attempts, method, path, tag: "cluster" });
1828
+ });
1829
+
1830
+ // src/proxmox/distilled-pve.ts
1831
+ class PveClusterExhausted extends Error {
1832
+ attempts;
1833
+ constructor(attempts) {
1834
+ super(`all members failed: ${attempts.map((a) => `${a.member} (${a.why})`).join("; ")}`);
1835
+ this.attempts = attempts;
1836
+ this.name = "PveClusterExhausted";
1837
+ }
1838
+ }
1839
+ var runPveWith = (target, credential, isWrite, op, timeout5 = MEMBER_TIMEOUT) => Effect7.gen(function* () {
1840
+ const attempts = [];
1841
+ for (const member of orderedMembers(target)) {
1842
+ const credLayer = distilledPveCredentials({
1843
+ baseUrl: pveApiBase(member),
1844
+ secret: credential.secret,
1845
+ tokenId: credential.tokenId
1846
+ });
1847
+ const outcome = yield* op.pipe(ProxmoxRetry.none, Effect7.provide(credLayer), Effect7.timeout(timeout5), Effect7.result);
1848
+ if (Result2.isSuccess(outcome)) {
1849
+ noteGoodMember(target, member);
1850
+ return outcome.success;
1851
+ }
1852
+ const mayRetry = isWrite ? isPreSendTransport(outcome.failure) : isTransportFailure(outcome.failure);
1853
+ if (!mayRetry)
1854
+ return yield* Effect7.fail(outcome.failure);
1855
+ attempts.push({ member, why: describeTransport(outcome.failure) });
1856
+ }
1857
+ return yield* Effect7.fail(new PveClusterExhausted(attempts));
1858
+ });
1859
+ var runPve = (target, role, isWrite, op) => Effect7.gen(function* () {
1860
+ const credential = yield* leased(target, role);
1861
+ return yield* runPveWith(target, credential, isWrite, op);
1862
+ });
1863
+
1864
+ // src/proxmox/unreadable-read.ts
1865
+ import * as Effect8 from "effect/Effect";
1866
+ var UNREADABLE = Symbol("pve-unreadable");
1867
+ var readOrUnreadable = (effect2) => effect2.pipe(Effect8.catchTag("PveCredentialDenied", () => Effect8.succeed(UNREADABLE)));
1868
+ var unreadableWarning = (type, identity, mount, tier) => Effect8.logWarning(`${type} ${identity}: the read of ${mount}/creds/${tier} was refused (no grant for this ` + "credential's role) -- reporting noop rather than forcing an update with nothing compared. " + "Re-plan on a lane that can read this object before trusting this row.");
1869
+
1870
+ // src/ceph/ceph-config-option-form.ts
1871
+ var configOptionProblem = (props) => {
1872
+ if (props.monNodes.length === 0)
1873
+ return "monNodes must name at least one mon ssh destination";
1874
+ const problem = cephCommandProblem(configSetArgv(props.who, props.name, props.value));
1875
+ return problem === undefined ? undefined : `Ceph.ConfigOption ${props.who}/${props.name}: ${problem}`;
1876
+ };
1877
+ var findLive = (rows, who, name) => {
1878
+ const row = rows.find((r) => r.section === who && r.name === name && (r.mask ?? "") === "");
1879
+ return row === undefined ? undefined : { name, value: String(row.value), who };
1880
+ };
1881
+ var matches = (live, props) => live !== undefined && live.value === props.value;
1882
+
1883
+ // src/ceph/ceph-config-option-reconcile.ts
1884
+ import * as Effect9 from "effect/Effect";
1885
+ var asError2 = (cause) => cause instanceof Error ? cause : new Error(String(cause));
1886
+ var runWrite = (transport, argv) => Effect9.gen(function* () {
1887
+ const result3 = yield* Effect9.tryPromise({
1888
+ catch: asError2,
1889
+ try: () => runCephCommand(transport, argv)
1890
+ });
1891
+ if (result3.exitCode !== 0) {
1892
+ return yield* Effect9.fail(new Error(`ceph ${argv.slice(1, 3).join(" ")} failed on mon ${result3.node} (exit ` + `${String(result3.exitCode)}): ${result3.stderr.trim().slice(0, 300)}`));
1893
+ }
1894
+ yield* Effect9.tryPromise({ catch: asError2, try: () => assertFreshQuorum(transport) });
1895
+ });
1896
+ var reconcileCephConfigOption = (props, readRows, deps) => Effect9.gen(function* () {
1897
+ const problem = configOptionProblem(props);
1898
+ if (problem !== undefined)
1899
+ return yield* Effect9.fail(new Error(problem));
1900
+ const live = findLive(yield* readRows, props.who, props.name);
1901
+ if (live !== undefined && matches(live, props))
1902
+ return live;
1903
+ yield* runWrite({ nodes: props.monNodes, ...deps }, configSetArgv(props.who, props.name, props.value));
1904
+ const after = findLive(yield* readRows, props.who, props.name);
1905
+ if (after === undefined || !matches(after, props)) {
1906
+ return yield* Effect9.fail(new Error(`Ceph.ConfigOption ${props.who}/${props.name}: 'ceph config set' succeeded but the mon ` + `config db reports ${after === undefined ? "no value" : JSON.stringify(after.value)}, ` + `not ${JSON.stringify(props.value)}. Not recorded — check \`ceph config dump\`.`));
1907
+ }
1908
+ return after;
1909
+ });
1910
+ var deleteCephConfigOption = (props, readRows, deps) => Effect9.gen(function* () {
1911
+ const live = findLive(yield* readRows, props.who, props.name);
1912
+ if (live === undefined)
1913
+ return;
1914
+ yield* runWrite({ nodes: props.monNodes, ...deps }, configRmArgv(props.who, props.name));
1915
+ });
1916
+
1917
+ // src/ceph/config-option.ts
1918
+ var CephConfigOption = Resource2("Ceph.ConfigOption", {
1919
+ defaultRemovalPolicy: "retain"
1920
+ });
1921
+ var readRows = (props) => runPve(props.target, "read", false, nodes.listNodeCephCfgDb({ node: props.node }));
1922
+ var refuse = (props) => {
1923
+ const problem = configOptionProblem(props);
1924
+ return problem === undefined ? Effect10.void : Effect10.fail(new Error(problem));
1925
+ };
1926
+ var CephConfigOptionProvider = () => Provider2.effect(CephConfigOption, Effect10.succeed(CephConfigOption.Provider.of({
1927
+ list: () => Effect10.succeed([]),
1928
+ read: Effect10.fn(function* ({ olds, output }) {
1929
+ const live = findLive(yield* readRows(olds), olds.who, olds.name);
1930
+ if (live === undefined)
1931
+ return;
1932
+ return output !== undefined ? live : Unowned(live);
1933
+ }),
1934
+ diff: Effect10.fn(function* ({ news, output }) {
1935
+ if (!isResolved2(news))
1936
+ return;
1937
+ yield* refuse(news);
1938
+ if (output === undefined)
1939
+ return;
1940
+ if (output.who !== news.who || output.name !== news.name)
1941
+ return { action: "replace" };
1942
+ const rows = yield* readOrUnreadable(readRows(news));
1943
+ if (rows === UNREADABLE) {
1944
+ yield* unreadableWarning("Ceph.ConfigOption", `${news.who}/${news.name}`, news.target.mount, mintTier(news.target, "read"));
1945
+ return { action: "noop" };
1946
+ }
1947
+ const live = findLive(rows, news.who, news.name);
1948
+ return { action: matches(live, news) ? "noop" : "update" };
1949
+ }),
1950
+ reconcile: ({ news }) => reconcileCephConfigOption(news, readRows(news), {}),
1951
+ delete: ({ olds }) => deleteCephConfigOption(olds, readRows(olds), {})
1952
+ })));
1601
1953
  export {
1602
1954
  CephAuthEntity,
1603
1955
  CephAuthEntityProvider,
1956
+ CephConfigOption,
1957
+ CephConfigOptionProvider,
1958
+ NAMED_CONFIG_OPTIONS,
1604
1959
  assertFreshQuorum,
1605
1960
  runCephCommand,
1606
1961
  sshCephDial
1607
1962
  };
1608
1963
 
1609
- //# debugId=9CE9ABE5EAEA3BE264756E2164756E21
1964
+ //# debugId=6A14FAFFC2879E5164756E2164756E21
1610
1965
  //# sourceMappingURL=index.js.map