@homeflare/alchemy 0.37.1 → 0.37.3
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/dist/proxmox/backup-job-config.d.ts +95 -0
- package/dist/proxmox/backup-job-config.d.ts.map +1 -0
- package/dist/proxmox/backup-job-distilled.d.ts +46 -0
- package/dist/proxmox/backup-job-distilled.d.ts.map +1 -0
- package/dist/proxmox/backup-job.d.ts +9 -26
- package/dist/proxmox/backup-job.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-distilled.d.ts +17 -10
- package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts +3 -3
- package/dist/proxmox/distilled-pbs.d.ts +12 -0
- package/dist/proxmox/distilled-pbs.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-jobs.d.ts +23 -0
- package/dist/proxmox/fake-pbs-jobs.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -1
- package/dist/proxmox/fake-pve.d.ts +1 -1
- package/dist/proxmox/fake-pve.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +2 -2
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +1382 -506
- package/dist/proxmox/index.js.map +51 -27
- package/dist/proxmox/live-core-fixtures.d.ts +40 -0
- package/dist/proxmox/live-core-fixtures.d.ts.map +1 -0
- package/dist/proxmox/metric-server-config.d.ts +49 -0
- package/dist/proxmox/metric-server-config.d.ts.map +1 -0
- package/dist/proxmox/metric-server-distilled.d.ts +43 -0
- package/dist/proxmox/metric-server-distilled.d.ts.map +1 -0
- package/dist/proxmox/metric-server.d.ts +2 -10
- package/dist/proxmox/metric-server.d.ts.map +1 -1
- package/dist/proxmox/node-network-wire.d.ts +1 -1
- package/dist/proxmox/node-network-wire.d.ts.map +1 -1
- package/dist/proxmox/notification-matcher-distilled.d.ts +9 -0
- package/dist/proxmox/notification-matcher-distilled.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher-lifecycle.d.ts +27 -0
- package/dist/proxmox/notification-matcher-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher.d.ts +5 -5
- package/dist/proxmox/notification-matcher.d.ts.map +1 -1
- package/dist/proxmox/notification-sdk-input.d.ts +22 -0
- package/dist/proxmox/notification-sdk-input.d.ts.map +1 -0
- package/dist/proxmox/notification-target-distilled.d.ts +12 -0
- package/dist/proxmox/notification-target-distilled.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +2 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/notification-target-lifecycle.d.ts +29 -0
- package/dist/proxmox/notification-target-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/notification-target-spec.d.ts +5 -0
- package/dist/proxmox/notification-target-spec.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +4 -5
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-endpoint.d.ts +1 -1
- package/dist/proxmox/pbs-datastore-form.d.ts +18 -42
- package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-guard.d.ts +7 -4
- package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-lifecycle.d.ts +56 -0
- package/dist/proxmox/pbs-datastore-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-wire.d.ts +21 -0
- package/dist/proxmox/pbs-datastore-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore.d.ts +5 -6
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-input.d.ts +18 -0
- package/dist/proxmox/pbs-notification-input.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher-distilled.d.ts +11 -0
- package/dist/proxmox/pbs-notification-matcher-distilled.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts +4 -3
- package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target-distilled.d.ts +20 -0
- package/dist/proxmox/pbs-notification-target-distilled.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +4 -4
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts +1 -1
- package/dist/proxmox/pbs-prune-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-prune-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-spec.d.ts +5 -0
- package/dist/proxmox/pbs-prune-job-spec.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-prune-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +9 -8
- package/dist/proxmox/pbs-prune-job.d.ts.map +1 -1
- package/dist/proxmox/pbs-sync-job-attributes.d.ts +34 -0
- package/dist/proxmox/pbs-sync-job-attributes.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts +21 -22
- package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-sync-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-sync-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-sync-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job.d.ts +9 -39
- package/dist/proxmox/pbs-sync-job.d.ts.map +1 -1
- package/dist/proxmox/pbs-verify-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-verify-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-spec.d.ts +16 -0
- package/dist/proxmox/pbs-verify-job-spec.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-verify-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job.d.ts +12 -12
- package/dist/proxmox/pbs-verify-job.d.ts.map +1 -1
- package/dist/proxmox/pool.d.ts +10 -1
- package/dist/proxmox/pool.d.ts.map +1 -1
- package/dist/proxmox/storage-wire.d.ts +2 -2
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/user-wire.d.ts +2 -2
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
- package/docs/changelog/README.md +22 -0
- package/docs/changelog/archive-01.md +176 -0
- package/docs/changelog/archive-02.md +180 -0
- package/docs/changelog/archive-03.md +179 -0
- package/docs/changelog/archive-04.md +178 -0
- package/docs/changelog/archive-05.md +177 -0
- package/docs/changelog/archive-06.md +180 -0
- package/docs/changelog/archive-07.md +180 -0
- package/docs/changelog/archive-08.md +178 -0
- package/docs/changelog/archive-09.md +181 -0
- package/docs/changelog/archive-10.md +182 -0
- package/docs/changelog/archive-11.md +182 -0
- package/docs/changelog/archive-12.md +183 -0
- package/docs/changelog/archive-13.md +180 -0
- package/docs/changelog/archive-14.md +178 -0
- package/docs/changelog/archive-15.md +181 -0
- package/docs/changelog/archive-16.md +182 -0
- package/docs/changelog/archive-17.md +183 -0
- package/docs/changelog/archive-18.md +55 -0
- package/docs/pbs-distilled-jobs.md +31 -0
- package/docs/pbs-notifications.md +8 -2
- package/docs/proxmox-notification-sdk.md +28 -0
- package/docs/proxmox-sdk-migration.md +60 -0
- package/docs/proxmox.md +9 -3
- package/docs/upstream-conformance-families.md +6 -2
- package/package.json +3 -3
- package/src/proxmox/adopt-noop.test.ts +4 -5
- package/src/proxmox/backup-job-config.ts +95 -0
- package/src/proxmox/backup-job-distilled.ts +86 -0
- package/src/proxmox/backup-job.ts +78 -116
- package/src/proxmox/ceph-fs-delete.test.ts +5 -8
- package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
- package/src/proxmox/ceph-fs-distilled.ts +41 -13
- package/src/proxmox/ceph-fs-wire.ts +5 -93
- package/src/proxmox/ceph-fs.ts +7 -7
- package/src/proxmox/constraints.test.ts +9 -8
- package/src/proxmox/distilled-pbs.test.ts +87 -0
- package/src/proxmox/distilled-pbs.ts +50 -0
- package/src/proxmox/fake-pbs-jobs.ts +99 -0
- package/src/proxmox/fake-pbs-notify.ts +4 -1
- package/src/proxmox/fake-pve.ts +3 -2
- package/src/proxmox/live-core-fixtures.ts +126 -0
- package/src/proxmox/live-core-lifecycle.test.ts +115 -0
- package/src/proxmox/live-core-wire.test.ts +190 -0
- package/src/proxmox/metric-server-config.ts +87 -0
- package/src/proxmox/metric-server-distilled.ts +81 -0
- package/src/proxmox/metric-server.ts +77 -88
- package/src/proxmox/notification-distilled.test.ts +162 -0
- package/src/proxmox/notification-matcher-distilled.ts +60 -0
- package/src/proxmox/notification-matcher-lifecycle.ts +71 -0
- package/src/proxmox/notification-matcher.ts +9 -26
- package/src/proxmox/notification-replacement.test.ts +190 -0
- package/src/proxmox/notification-sdk-input.ts +39 -0
- package/src/proxmox/notification-target-distilled.ts +160 -0
- package/src/proxmox/notification-target-form.ts +2 -0
- package/src/proxmox/notification-target-lifecycle.ts +75 -0
- package/src/proxmox/notification-target-spec.ts +68 -0
- package/src/proxmox/notification-target.ts +6 -68
- package/src/proxmox/pbs-datastore-endpoint.ts +1 -1
- package/src/proxmox/pbs-datastore-form.ts +18 -49
- package/src/proxmox/pbs-datastore-guard.ts +7 -4
- package/src/proxmox/pbs-datastore-lifecycle.ts +114 -0
- package/src/proxmox/pbs-datastore-wire.ts +94 -0
- package/src/proxmox/pbs-datastore.ts +7 -133
- package/src/proxmox/pbs-jobs-delete.test.ts +40 -0
- package/src/proxmox/pbs-jobs-sdk.test.ts +215 -0
- package/src/proxmox/pbs-notification-errors.test.ts +93 -0
- package/src/proxmox/pbs-notification-input.ts +25 -0
- package/src/proxmox/pbs-notification-mail.test.ts +64 -0
- package/src/proxmox/pbs-notification-matcher-distilled.ts +65 -0
- package/src/proxmox/pbs-notification-matcher.ts +70 -17
- package/src/proxmox/pbs-notification-target-distilled.ts +124 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +26 -17
- package/src/proxmox/pbs-notification-target.ts +1 -1
- package/src/proxmox/pbs-notifications-stack.test.ts +25 -0
- package/src/proxmox/pbs-prune-job-lifecycle.ts +52 -0
- package/src/proxmox/pbs-prune-job-spec.ts +53 -0
- package/src/proxmox/pbs-prune-job-wire.ts +68 -0
- package/src/proxmox/pbs-prune-job.ts +10 -58
- package/src/proxmox/pbs-sync-job-attributes.ts +33 -0
- package/src/proxmox/pbs-sync-job-form.ts +7 -6
- package/src/proxmox/pbs-sync-job-lifecycle.ts +46 -0
- package/src/proxmox/pbs-sync-job-wire.ts +71 -0
- package/src/proxmox/pbs-sync-job.ts +10 -41
- package/src/proxmox/pbs-verify-job-lifecycle.ts +52 -0
- package/src/proxmox/pbs-verify-job-spec.ts +76 -0
- package/src/proxmox/pbs-verify-job-wire.ts +58 -0
- package/src/proxmox/pbs-verify-job.ts +13 -85
- package/src/proxmox/pool.ts +92 -10
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Alchemy changelog archive 2
|
|
2
|
+
|
|
3
|
+
[Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
|
|
4
|
+
|
|
5
|
+
The read (the directory index, `nodes.listNodeCephFs`) and the create (`nodes.updateNodeCephFs` —
|
|
6
|
+
distilled's generator MISNAMES the create a "update"; checked directly against its `T.Http`
|
|
7
|
+
annotation, it is genuinely `POST /nodes/{node}/ceph/fs/{name}`, PVE's only write on that path,
|
|
8
|
+
harmless once named for what it is) both move to a new `ceph-fs-distilled.ts`. The create's forked-
|
|
9
|
+
worker wait is its OWN function there, polling the RAW `nodes.getNodeTaskStatus` operation directly
|
|
10
|
+
rather than `@distilled.cloud/proxmox`'s own `Task.awaitTask` — checked against `task.ts` before
|
|
11
|
+
deciding: `Task.awaitTask` propagates a poll failure as a genuine typed error instead of treating
|
|
12
|
+
it as "not settled yet", which would turn a missing `Sys.Audit` grant or a node mid-restart into a
|
|
13
|
+
failed create instead of a patient one, the same failure mode `Proxmox.NetworkApply`'s own header
|
|
14
|
+
already names. The raw operation carries none of that behaviour — it is a plain typed GET — so
|
|
15
|
+
wrapping it in the SAME tolerant retry loop `ceph-fs-wire.ts`'s own (unmigrated, delete-only) poll
|
|
16
|
+
already uses reproduces the exact policy: same cadence (`POLL_SECONDS`/`POLL_ATTEMPTS`, one
|
|
17
|
+
constant, imported not retyped), same "an unreadable status is not a failure" tolerance.
|
|
18
|
+
|
|
19
|
+
**Live measurement note.** This session's own OpenBao policy has no Proxmox grant; the live probes
|
|
20
|
+
above ran through `homeflare-proxmox`'s `scripts/with-mini-bao.ts` (the `macmini-agent` AppRole's
|
|
21
|
+
`read` role for TB4), from a detached worktree, read-only, per the coordinator's direction — not
|
|
22
|
+
through this repo's own credentials, and nothing here reads one.
|
|
23
|
+
|
|
24
|
+
**One distilled generator quirk recorded to the shared `distilled-sdk-path.md` memory**, since it
|
|
25
|
+
is an upstream-distilled fact rather than a kit one: `DeleteNodeCephFsRequest` types
|
|
26
|
+
`remove_pools`/`remove_storages` as `T.Body` on a DELETE, which PVE's own server never reads a
|
|
27
|
+
body on — the same class of finding as `Proxmox.NetworkApply`'s two, worth the same upstream
|
|
28
|
+
attention if `@distilled.cloud/proxmox` is ever patched again.
|
|
29
|
+
|
|
30
|
+
**New tests**, every one proven against a deliberately weakened check before landing:
|
|
31
|
+
`ceph-pool-write.test.ts` (a genuinely new pool creates once the index is confirmed empty; the
|
|
32
|
+
PG-merge guard refuses a create when the index lists the name but the status read still fails —
|
|
33
|
+
confirmed this fails without the guard by temporarily removing the `confirmAbsent` call and
|
|
34
|
+
re-running; `RemovalPolicy.destroy()` then undeclaring sends exactly one DELETE) and
|
|
35
|
+
`ceph-fs-write.test.ts` (a genuinely new filesystem POSTs, polls a forked task that answers
|
|
36
|
+
`running` before `stopped`/`OK` — proving the poll loop actually loops — and reads the index back;
|
|
37
|
+
a task that finishes with a real Ceph error dies with the task-log pointer rather than a false
|
|
38
|
+
create).
|
|
39
|
+
|
|
40
|
+
**`ceph-fs-delete.test.ts` (new, added after the adversarial review's finding).** The review's one
|
|
41
|
+
substantive finding: `Proxmox.CephFs`'s `delete` handler is the single place in this PR that
|
|
42
|
+
composes a distilled call (`readFs`, before and after) with the unmigrated hand-client one
|
|
43
|
+
(`destroyFs`) inside one handler — the exact boundary this PR's whole design rests on — and that
|
|
44
|
+
composition had zero test coverage anywhere in the repo, before or after the rest of this PR
|
|
45
|
+
(each half was proven separately, never together, and the destroy is destructive and hard to
|
|
46
|
+
reverse). Added two tests: a genuine `RemovalPolicy.destroy()` then undeclare, confirmed to fail
|
|
47
|
+
(the read-back reports the filesystem still present) by temporarily removing the `destroyFs` call
|
|
48
|
+
and re-running; and a filesystem removed by hand between the adopt and the undeclare, proving
|
|
49
|
+
`delete` is a silent no-op rather than a false DELETE against an object already gone.
|
|
50
|
+
|
|
51
|
+
**Expected after this releases and the consumer bumps:** no live plan change for either family —
|
|
52
|
+
C1's read-role lease could always read the live Ceph objects cleanly, so this is a transport
|
|
53
|
+
migration on tested paths, not a behaviour change.
|
|
54
|
+
|
|
55
|
+
- [#261](https://github.com/taslabs-net/homeflare-kit/pull/261) [`ee3f452`](https://github.com/taslabs-net/homeflare-kit/commit/ee3f4526d8fa2991837a8d7931b2fb37df921eda) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `@distilled.cloud/litellm` (interim, via `packages/distilled-litellm`): reverts `credentials.ts`
|
|
56
|
+
to `Effect.orDie` on a missing or misspelled `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY`.
|
|
57
|
+
|
|
58
|
+
Q6 of the 2026-09-24 walk-down (decision 49 "upstream wins"): distilled's own convention for a
|
|
59
|
+
missing credential is `Effect.orDie`, not a typed `ConfigError` — 73 of 80 `packages/*/src/credentials.ts`
|
|
60
|
+
end this way at `homeflare/base`, 78 of 80 at `origin/main`. A worktree commit (`4ad19154`) had
|
|
61
|
+
made LiteLLM's `Credentials` the one exception, declaring `Effect.Effect<Config, ConfigError>` and
|
|
62
|
+
dropping the `orDie`. Distilled is not silent on this convention, so the divergence was the house
|
|
63
|
+
going stricter than distilled's own practice on a point it already has an answer for — the thing
|
|
64
|
+
decision 49 says to stop doing. Reverted in the litellm worktree (`homeflare/litellm`
|
|
65
|
+
`cbd6bbb890066e57fb0224b96c27fca91d89e0a6`, a `git revert` of `4ad19154`, confirmed byte-identical
|
|
66
|
+
to `4ad19154`'s parent), gated on `typecheck:ci`/`specs:check`/`format:check`, then byte-copied
|
|
67
|
+
into `packages/distilled-litellm/src/credentials.ts` (confirmed with `diff -rq` against the
|
|
68
|
+
worktree's `src/`).
|
|
69
|
+
|
|
70
|
+
**The typed-error idea is not discarded**, just not spread further: it is written up as a HELD
|
|
71
|
+
upstream proposal in [`litellm.md`](../litellm.md#credentials) — distilled's
|
|
72
|
+
own `LitellmOpError` already declares `ConfigError` and threads a real credentials failure into an
|
|
73
|
+
operation's error channel, so the plumbing for a typed refusal genuinely exists; nobody has raised
|
|
74
|
+
it to `alchemy-run/distilled`, and this PR doesn't either.
|
|
75
|
+
|
|
76
|
+
**`packages/alchemy/src/litellm/operations.ts`'s `mutate` helper needed no functional change.**
|
|
77
|
+
Its `| ConfigError` widening was always redundant with the operation-level `LitellmOpError` union
|
|
78
|
+
(which declares `ConfigError` unconditionally, regardless of `Credentials`'s own type) — confirmed
|
|
79
|
+
by re-reading `protocol.ts` — so it still typechecks and the widening is still a real no-op for
|
|
80
|
+
callers. Its comment is corrected: it no longer claims resolving `Credentials` can itself fail
|
|
81
|
+
typed, since post-revert it cannot.
|
|
82
|
+
|
|
83
|
+
**Verified**: `bun run build:interim-packages`, `tsc --noEmit` for the whole `@homeflare/alchemy`
|
|
84
|
+
package, `bun test src/litellm` (28/28), and `packages/distilled-litellm`'s own `types` and
|
|
85
|
+
`smoke` — all clean.
|
|
86
|
+
|
|
87
|
+
No live plan change: this only affects what happens when the two env vars are missing or
|
|
88
|
+
misspelled, which was never a state this family's tests exercised as "succeeds."
|
|
89
|
+
|
|
90
|
+
- [#266](https://github.com/taslabs-net/homeflare-kit/pull/266) [`c6f3f9a`](https://github.com/taslabs-net/homeflare-kit/commit/c6f3f9a33fa2ed72c5d73586fa74090582330cd0) Thanks [@taslabs-net](https://github.com/taslabs-net)! - Make `Cloudflare.R2BucketLock` deletion succeed when its bucket is already gone.
|
|
91
|
+
Read, reconcile and delete now propagate distilled's typed SDK failures instead of
|
|
92
|
+
turning them into `Effect.orDie` defects. Error identities such as `InvalidRoute`,
|
|
93
|
+
`TooManyRequests` and reconcile's `NoSuchBucket` remain available to callers.
|
|
94
|
+
|
|
95
|
+
This follows the lifecycle pattern in upstream `Cloudflare/R2/BucketSippy.ts` at
|
|
96
|
+
`alchemy@2.0.0-beta.79` (`473c3959`): catch the missing bucket only where it means
|
|
97
|
+
absence or successful deletion, and propagate other SDK errors without a blanket remap.
|
|
98
|
+
Walked against Cloudflare API v4 via `@distilled.cloud/cloudflare@1.0.0-rc.12`,
|
|
99
|
+
the version pinned by Alchemy beta.79. No props, attributes or SDK pins change.
|
|
100
|
+
|
|
101
|
+
Fake-transport tests cover missing-bucket reads and deletes, missing-bucket reconcile
|
|
102
|
+
failures, other 404 errors, and rate-limited reads/deletes with retries disabled.
|
|
103
|
+
|
|
104
|
+
## 0.36.0
|
|
105
|
+
|
|
106
|
+
### Minor Changes
|
|
107
|
+
|
|
108
|
+
- [#255](https://github.com/taslabs-net/homeflare-kit/pull/255) [`74310b5`](https://github.com/taslabs-net/homeflare-kit/commit/74310b5fcbd6ab1ddbebe607c2693c34d664bf30) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `grafana/*` ships `Grafana.NotificationPolicy` — third and last of the stacked PRs bringing Grafana
|
|
109
|
+
alerting provisioning under Alchemy (decision 40), completing the family alongside
|
|
110
|
+
`Grafana.ContactPoint`/`Grafana.MuteTiming`/`Grafana.MessageTemplate` (kit PR 250) and
|
|
111
|
+
`Grafana.AlertRuleGroup` (kit PR 254). Full detail, including what was measured against the SDK's
|
|
112
|
+
generated types versus Grafana's own docs (no live call was made in this PR):
|
|
113
|
+
`docs/grafana-notification-policy.md`.
|
|
114
|
+
|
|
115
|
+
**A singleton — adopt and update only.** Measured: `RouteGetPolicyTreeError` has no `NotFound`
|
|
116
|
+
case, so `fetchLive` never returns `undefined` — this resource has no create or delete in the sense
|
|
117
|
+
every sibling resource has, only ever `PUT`-replaces the one tree an instance has. `spec.create`
|
|
118
|
+
exists only because `GrafanaSpec`'s shared type requires it; it is never actually reached.
|
|
119
|
+
|
|
120
|
+
**`routeResetPolicyTree` needs TWO explicit gates, not one.** `defaultRemovalPolicy: 'retain'`
|
|
121
|
+
means Alchemy's engine never calls `destroy` at all unless a stack opts in with
|
|
122
|
+
`.pipe(RemovalPolicy.destroy())` — the convention every multi-object-cascade resource in this
|
|
123
|
+
family already uses. On top of that, `destroy` itself refuses to reset unless the resource's own
|
|
124
|
+
`allowReset: true` prop is set — even a stack that opted in at the Alchemy level still needs this.
|
|
125
|
+
Without it, `destroy` logs a warning and retains the tree. Two gates because `routeResetPolicyTree`
|
|
126
|
+
is uniquely destructive here: it wipes every route, receiver reference and matcher back to
|
|
127
|
+
Grafana's bare default for the WHOLE instance, not one object. A test proves `destroy` sends no
|
|
128
|
+
`DELETE` without `allowReset`, and that a foreign-provenance tree still refuses destroy even WITH
|
|
129
|
+
it.
|
|
130
|
+
|
|
131
|
+
**The tree is opaque `Record<string, unknown>`**, same reasoning as `Grafana.MuteTiming`'s
|
|
132
|
+
`timeIntervals` — Grafana's matcher-expression types (`Matchers`/`ObjectMatchers`/`MatchRegexps`)
|
|
133
|
+
are complex nested unions this family does not hand-model, and the SDK's own unmodeled-key
|
|
134
|
+
passthrough (measured for `Grafana.MuteTiming`, mute-timing.ts's file header) means an opaque
|
|
135
|
+
pass-through round-trips correctly regardless.
|
|
136
|
+
|
|
137
|
+
**Route order is significant**, same as `Grafana.AlertRuleGroup`'s rules — Alertmanager routes an
|
|
138
|
+
alert to the first matching route in array order, proven by a test that reverses the same two
|
|
139
|
+
routes and asserts a mismatch. **Foreign-provenance refusal** reuses `alerting-provenance.ts`
|
|
140
|
+
unchanged, including the missing-means-foreign fix an adversarial review of kit PR 250 established.
|
|
141
|
+
**Grafana-injected defaults** are tolerated the `subset-match.ts` way (the same mechanism
|
|
142
|
+
`Grafana.Dashboard`/`Grafana.ContactPoint` already rely on), not hand-normalized — not measured
|
|
143
|
+
against a live instance, since none exists in this house to read.
|
|
144
|
+
|
|
145
|
+
**Dropped-route warning**, mirroring kit PR 254's `Grafana.AlertRuleGroup` fix for the identical
|
|
146
|
+
risk: a whole-tree `PUT` replaces every route, so a nested route added out-of-band is removed with
|
|
147
|
+
no error. `notification-policy-drop-warning.ts`'s `warnOnDroppedRoutes` logs every dropped route by
|
|
148
|
+
receiver and dotted-index path, from both a local reimplementation of `diff` and from `update`
|
|
149
|
+
itself — never a refusal, since the tree stays the unit by design.
|
|
150
|
+
|
|
151
|
+
⛔ **Fixed after an adversarial review of this PR: the identity the warning used collided across
|
|
152
|
+
the WHOLE TREE, not just among siblings.** The first version keyed every node by its own-fields
|
|
153
|
+
JSON alone in a `Set`; a duplicate route added anywhere in the tree matched an unrelated declared
|
|
154
|
+
node with the same fields and the drop went unwarned, even though the `PUT` still removes it —
|
|
155
|
+
worse than the "structural, siblings-only" limitation the original comment claimed. `FlatRoute.key`
|
|
156
|
+
(`notification-policy-model.ts`) is now the chain of ancestor own-fields from the root down to a
|
|
157
|
+
node (`\0`-joined) — order-insensitive among siblings, but distinct across different parents — and
|
|
158
|
+
`warnOnDroppedRoutes` counts each key as a MULTISET (live count vs. declared count) rather than
|
|
159
|
+
checking `Set` membership, so N identical live occurrences under one parent are dropped only past
|
|
160
|
+
however many the declaration itself repeats. Four tests pin this: a duplicate added live warns; a
|
|
161
|
+
plain sibling reorder doesn't; an identical route under a different, undeclared parent still warns;
|
|
162
|
+
and two declared duplicates against two live duplicates warns nothing.
|
|
163
|
+
|
|
164
|
+
Tests (`notification-policy.test.ts` + `-model.test.ts` + `-refusals.test.ts` +
|
|
165
|
+
`-drop-warning.test.ts`, split four ways to stay under the house's 250-line file cap, sharing
|
|
166
|
+
fixtures from `notification-policy-fixtures.ts`) use the family's existing `fake-grafana.ts`
|
|
167
|
+
harness and prove: a GET never carries a body; reconcile never sends a create-shaped write; a
|
|
168
|
+
foreign-provenance tree (explicit `"file"` or MISSING) refuses update while an explicit `""` is
|
|
169
|
+
writable; `destroy` never resets without `allowReset: true`; reordering routes shows `update`; and
|
|
170
|
+
the dropped-route warning's multiset identity behaves as described above.
|
|
171
|
+
|
|
172
|
+
## 0.35.0
|
|
173
|
+
|
|
174
|
+
### Minor Changes
|
|
175
|
+
|
|
176
|
+
- [#254](https://github.com/taslabs-net/homeflare-kit/pull/254) [`e6788af`](https://github.com/taslabs-net/homeflare-kit/commit/e6788af70a34495af64d9bc0a4fce112e59bf561) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `grafana/*` ships `Grafana.AlertRuleGroup` — second of three stacked PRs bringing Grafana alerting
|
|
177
|
+
provisioning under Alchemy (decision 40), on the same `@distilled.cloud/grafana@0.2.0` operations
|
|
178
|
+
as `Grafana.ContactPoint`/`Grafana.MuteTiming`/`Grafana.MessageTemplate`. `Grafana.NotificationPolicy`
|
|
179
|
+
follows in one more PR. Full detail, including what was measured against the SDK's generated types
|
|
180
|
+
versus Grafana's own docs (no live call was made in this PR): `docs/grafana-alerting-rules.md`.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Alchemy changelog archive 3
|
|
2
|
+
|
|
3
|
+
[Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
|
|
4
|
+
|
|
5
|
+
**The unit is the group, not the rule** — measured: `PUT /v1/provisioning/folder/{FolderUID}/
|
|
6
|
+
rule-groups/{Group}` takes the evaluation `interval` AND the full ordered `rules[]` together,
|
|
7
|
+
replacing the whole group in one call; the individual per-rule operations have no `interval` field
|
|
8
|
+
at all and cannot reorder rules within a group. Declaring rules one at a time would leave a group's
|
|
9
|
+
interval and rule order either undeclarable or racy across independent resources with no shared
|
|
10
|
+
source of truth. Mirrors Terraform's own `grafana_rule_group` resource.
|
|
11
|
+
|
|
12
|
+
Rule order is significant and never reordered — `subset-match.ts`'s existing position-significant
|
|
13
|
+
array comparison, proven by a dedicated test that declares the same two rules reversed and asserts
|
|
14
|
+
a mismatch. `uid` is required on every declared rule (this family's doctrine everywhere else).
|
|
15
|
+
Volatile fields (`id`, `updated`, `provenance`) are stripped before comparison by a new
|
|
16
|
+
`normalizeRule` (`alert-rule-group-model.ts`, mirroring `dashboard-model.ts`'s `normalizeModel`),
|
|
17
|
+
each measured against the read vs. write SDK shapes; `folderUID`/`ruleGroup` are forced to the
|
|
18
|
+
group's own canonical values and `orgID` defaults to `1` (not measured against a multi-org
|
|
19
|
+
instance — none exists in this house yet).
|
|
20
|
+
|
|
21
|
+
**Foreign-provenance refusal is per rule, whole-group refuse.** The `AlertRuleGroup` type itself
|
|
22
|
+
carries no top-level `provenance` field (measured) — each rule inside `rules[]` has its own, and
|
|
23
|
+
Grafana's docs say a new rule's provenance must match its group's, so this resource checks every
|
|
24
|
+
live rule via the shared `alerting-provenance.ts` (including the missing-means-foreign fix an
|
|
25
|
+
adversarial review of kit PR 250 established) and refuses the whole group's write if any rule is
|
|
26
|
+
foreign, naming the first one found. An empty live group has nothing to refuse on.
|
|
27
|
+
|
|
28
|
+
**A declared group whose folder does not exist refuses — it is never auto-created.** This resource
|
|
29
|
+
makes no folder-existence check of its own and never invokes `Grafana.Folder`'s create path.
|
|
30
|
+
Measured: `RoutePutAlertRuleGroupError` is `BadRequest | Forbidden | GrafanaOpError` with no
|
|
31
|
+
`NotFound` case, so a nonexistent `folderUid` fails the PUT as a typed, uncaught `BadRequest`
|
|
32
|
+
rather than being folded to absent or silently scaffolded — declare a `Grafana.Folder` first.
|
|
33
|
+
|
|
34
|
+
Delete defaults to `retain` (`defaultRemovalPolicy`) — deleting a group deletes every rule in it in
|
|
35
|
+
one call, the same multi-object-cascade class `Grafana.Folder` already guards this way.
|
|
36
|
+
|
|
37
|
+
⛔ **Fixed after an adversarial review of this PR: a whole-group write silently dropped any live
|
|
38
|
+
rule not in the declaration.** A human adding a rule in the Grafana UI minutes before a deploy
|
|
39
|
+
(ordinary, writable provenance — nothing for the foreign-provenance refusal to catch) would have
|
|
40
|
+
had it removed with no error, and `diff` only ever said `"update"` with no hint why —
|
|
41
|
+
`alchemy/Diff`'s `Diff` type has no fourth action to carry a lossy update through as. Fixed with a
|
|
42
|
+
loud, non-blocking warning (`alert-rule-group-drop-warning.ts`'s `warnOnDroppedRules`, mirroring
|
|
43
|
+
`proxmox/unreadable-read.ts`'s established pattern): every dropped rule is named by title and uid
|
|
44
|
+
in an `Effect.logWarning`, logged from both a local reimplementation of `diff` (so `bun run plan`
|
|
45
|
+
shows it before anything is written) and from `update` itself. This does NOT refuse the write —
|
|
46
|
+
the group is still the unit, by design — only makes the loss visible.
|
|
47
|
+
|
|
48
|
+
Tests (`alert-rule-group.test.ts` + `alert-rule-group-model.test.ts` +
|
|
49
|
+
`alert-rule-group-provenance.test.ts`, split three ways to stay under the house's 250-line file cap,
|
|
50
|
+
sharing fixtures from `alert-rule-group-fixtures.ts`) use the family's existing `fake-grafana.ts`
|
|
51
|
+
harness and prove: a GET never carries a body; a nonexistent folder propagates `BadRequest`; a
|
|
52
|
+
foreign-provenance rule (explicit or missing) refuses the whole group while an explicit `""` is
|
|
53
|
+
writable; reordering the same rules shows `update`; and an unchanged declaration is a noop despite
|
|
54
|
+
every rule's `id`/`updated`/`provenance` differing.
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- [#251](https://github.com/taslabs-net/homeflare-kit/pull/251) [`4ef7909`](https://github.com/taslabs-net/homeflare-kit/commit/4ef790926bdcf5033aae1e31585e9490803aadf7) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `proxmox/*` family's `nodes/storage` sub-area, third resource (decision 43's serial proxmox
|
|
59
|
+
walk-down, after `access` in PR 231/239, `storage` in PR 243 and `ZfsPool` in PR 246): migrates
|
|
60
|
+
`Proxmox.NodeNetwork` off `client.ts`'s hand-rolled `pve()`/`pveHandlers` onto
|
|
61
|
+
`@distilled.cloud/proxmox`'s typed `nodes.getNodeNetwork`/`createNodeNetwork`/`putNodeNetwork2`/
|
|
62
|
+
`deleteNodeNetwork2`. Split into `node-network.ts` (the resource and its provider),
|
|
63
|
+
`node-network-wire.ts` (the read side — `readInterfaceOrFail`/`readInterface`, `matches`,
|
|
64
|
+
`attributesOf`) and `node-network-form.ts` (`NodeNetworkProps`, the write side, and the
|
|
65
|
+
coercion helpers both other files need) — the storage.ts/storage-form.ts seam.
|
|
66
|
+
|
|
67
|
+
**`Proxmox.NetworkApply` stays on `client.ts`, deliberately, not migrated with this PR.** Decision
|
|
68
|
+
28/29/9 ("NetworkApply stays an operator step") plus two MEASURED reasons, checked directly
|
|
69
|
+
against `@distilled.cloud/proxmox`'s own source before deciding, not assumed:
|
|
70
|
+
|
|
71
|
+
1. `network-apply-read.ts`'s `stagedDiff` reads the ONLY signal this family has for "is anything
|
|
72
|
+
staged" — `changes`, a SIBLING of `data` in PVE's own envelope
|
|
73
|
+
(`{"data":[...],"changes":"<unified diff>"}`), not inside it. `@distilled.cloud/proxmox`'s
|
|
74
|
+
`protocol.ts` `transformResponse` unconditionally returns `body.data ?? {}` for EVERY operation
|
|
75
|
+
on the protocol, discarding any sibling field before a generated operation's typed output is
|
|
76
|
+
even built. There is no typed call, current or future, that could see `changes` without a
|
|
77
|
+
protocol-level change upstream in the distilled package itself.
|
|
78
|
+
2. `network-apply-read.ts`'s hand-rolled `awaitTask` deliberately collapses a failed poll to
|
|
79
|
+
"outcome unknown" rather than "task failed", because the member serving the poll can be the
|
|
80
|
+
very node whose reload just dropped the connection the poll rides on. `@distilled.cloud/
|
|
81
|
+
proxmox`'s own `Task.awaitTask` (`src/task.ts`) does not have this behaviour — it propagates a
|
|
82
|
+
poll failure as a genuine typed `GetNodeTaskStatusError`, which would fail `NetworkApply`'s
|
|
83
|
+
`reconcile` outright on the one failure mode it most needs to tolerate.
|
|
84
|
+
|
|
85
|
+
Both gaps are documented in `network-apply-read.ts`'s own header now, next to the functions they
|
|
86
|
+
affect. Revisit when distilled's protocol layer exposes the raw envelope, or its `awaitTask` grows
|
|
87
|
+
an option to treat a poll failure as unknown rather than fatal.
|
|
88
|
+
|
|
89
|
+
**A missing interface is a 400, not a 404 and not the 500 every other migrated family has** —
|
|
90
|
+
MEASURED against the live cluster (TB4 `n2`, read-role, read-only probe): `GET
|
|
91
|
+
/nodes/n2/network/vmbr9` answers `{"errors":{"iface":"interface does not exist"},"data":null,
|
|
92
|
+
"message":"Parameter verification failed.\n"}` at HTTP 400. `@distilled.cloud/proxmox` types PVE's
|
|
93
|
+
400 as a genuine typed error, `ParameterVerificationFailed`, carrying the per-field `errors`
|
|
94
|
+
object rather than collapsing it to an opaque `BadRequest`. Only that exact signal —
|
|
95
|
+
`error.errors.iface === 'interface does not exist'` — means absent; a different 400 reason, a
|
|
96
|
+
genuine 500, or a network blip all propagate as real failures rather than being read as "gone".
|
|
97
|
+
|
|
98
|
+
**A precise absence signal does not remove the need for the dual-path read.** A first draft
|
|
99
|
+
reasoned that a parsed, specific absence signal made `readXOrFail`/folding-`readX` and `read`'s
|
|
100
|
+
`output`-branching unnecessary — every other migrated family carries that pair for the
|
|
101
|
+
`Drift.ts` gap, but this family's absence check already rejects anything that is not the exact
|
|
102
|
+
measured 400, so nothing wrongly propagates as absence. A Sonnet adversarial review caught the
|
|
103
|
+
flaw: precision of the absence signal and safety of folding at a given call site are separate
|
|
104
|
+
questions. Three of Alchemy's four `read` call sites (`Plan.ts`'s cold-start adoption probe,
|
|
105
|
+
`Plan.ts`'s interrupted-create recovery, `Apply.ts`'s delete recovery) call `provider.read` with
|
|
106
|
+
no state to compare against yet and no catch of their own around a typed failure — and `Plan.ts`
|
|
107
|
+
aggregates every resource's diff/probe effects fail-fast, so one resource's malformed or
|
|
108
|
+
transiently-failing declaration would abort the whole plan. Folding at those sites is what turns
|
|
109
|
+
that into a normal `create`/`noop` instead. Fixed by restoring the pair:
|
|
110
|
+
`readInterfaceOrFail` (non-folding, used by `diff` always and by `reconcile`'s write path) and
|
|
111
|
+
`readInterface` (folding, used by `read` when `output` is `undefined`, mirroring
|
|
112
|
+
user.ts/group.ts/storage.ts/zfs-pool.ts). `node-network-wire.ts`'s own header on both functions
|
|
113
|
+
has the full reasoning.
|
|
114
|
+
|
|
115
|
+
**TB4's fabric ports stay excluded, unchanged.** `tb0`/`tb1`/`dummy_c1` (the Thunderbolt mesh
|
|
116
|
+
SdnFabric owns, per decision 28/29/9) were never declared through this family and still are not —
|
|
117
|
+
this migration touches only how an interface this family DOES manage gets read and written, never
|
|
118
|
+
what gets declared through it. The header's own warning about them is carried forward verbatim,
|
|
119
|
+
now with an explicit pointer to decision 28/29/9.
|
|
120
|
+
|
|
121
|
+
Bugs caught before opening the PR, all fixed: an early draft's `diff` forgot to call
|
|
122
|
+
`unreadableWarning` on a refused-credential row (every OTHER migrated family logs it; this one
|
|
123
|
+
silently didn't) — self-caught, re-running the full test suite, fixed and now covered by
|
|
124
|
+
`node-network-read-failure.test.ts`'s own cries-wolf test. An unused import
|
|
125
|
+
(`ParameterVerificationFailed` imported as a value, needed only as the string tag
|
|
126
|
+
`Effect.catchTag` matches against) — caught by the pre-commit lint gate. And the dual-path
|
|
127
|
+
regression above — caught by the adversarial review, not self-caught.
|
|
128
|
+
|
|
129
|
+
`node-network-read-failure.test.ts` (new — this family had no dedicated test file before this
|
|
130
|
+
migration, only the create-form's vendor-constraint proof in `constraints-host-forms.test.ts`)
|
|
131
|
+
pins the cries-wolf fix, the precise absence signal (a genuinely new interface creates from
|
|
132
|
+
exactly that 400; a different 400 and a genuine 500 both propagate instead of creating), and the
|
|
133
|
+
`alchemy drift` fix. Each assertion was confirmed to fail against a deliberately weakened check
|
|
134
|
+
before landing (the "different 400 propagates" test specifically catches a version of the absence
|
|
135
|
+
check that matches any 400, not just the measured one).
|
|
136
|
+
|
|
137
|
+
**Expected after this releases and the consumer bumps:** no live plan change is expected for
|
|
138
|
+
`Proxmox.NodeNetwork` specifically — its `read`-role lease could always read the family's three
|
|
139
|
+
declared interfaces cleanly, so this is a transport migration plus a defensive fix for a bug class
|
|
140
|
+
not yet measured live for this family. `Proxmox.NetworkApply`'s own plan is unaffected, since it
|
|
141
|
+
is untouched by this PR.
|
|
142
|
+
|
|
143
|
+
## 0.34.0
|
|
144
|
+
|
|
145
|
+
### Minor Changes
|
|
146
|
+
|
|
147
|
+
- [#250](https://github.com/taslabs-net/homeflare-kit/pull/250) [`0e9c010`](https://github.com/taslabs-net/homeflare-kit/commit/0e9c0109ea21ffaf142db35f6a19bd424b23b9c2) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `grafana/*` ships `Grafana.ContactPoint`, `Grafana.MuteTiming` and `Grafana.MessageTemplate` — the
|
|
148
|
+
first of three stacked PRs bringing Grafana alerting provisioning under Alchemy (decision 40: every
|
|
149
|
+
internal alert lands in one place), on the same `@distilled.cloud/grafana@0.2.0` operations that
|
|
150
|
+
unblocked `Grafana.Folder`/`Grafana.Dashboard`. `Grafana.AlertRuleGroup` and
|
|
151
|
+
`Grafana.NotificationPolicy` follow in two later PRs. Full detail, including what was measured
|
|
152
|
+
against the SDK's generated types versus Grafana's own docs (no live call was made in this PR):
|
|
153
|
+
`docs/grafana-alerting.md`.
|
|
154
|
+
|
|
155
|
+
**Shared across the family:** a foreign-provenance refusal (`alerting-provenance.ts`) — every
|
|
156
|
+
resource reads Grafana's own `provenance` field and refuses a write when it names an owner other
|
|
157
|
+
than this API (an allowlist of `""`/`"api"`, not a denylist of the known foreign values, so an
|
|
158
|
+
unnamed future provenance source refuses by default). This is defense in depth: Grafana's alerting
|
|
159
|
+
provisioning API also enforces it server-side, unlike the classic folder/dashboard write API. No
|
|
160
|
+
resource in this family ever sets `X-Disable-Provenance` — an object this family declares stays
|
|
161
|
+
owned by the declaration.
|
|
162
|
+
|
|
163
|
+
⛔ **Fixed after an adversarial review of this PR: the guard failed OPEN.** The first version
|
|
164
|
+
mapped `live.provenance ?? ''`, so a response that simply omitted the field was indistinguishable
|
|
165
|
+
from an explicit `""` (`ProvenanceNone`, writable) — a real risk for `Grafana.MuteTiming`
|
|
166
|
+
specifically, whose `provenance` isn't even in the SDK's declared type and survives only through an
|
|
167
|
+
unmodeled-key passthrough this house has never observed against a live response. `undefined` is now
|
|
168
|
+
kept as its own value all the way through `isForeignProvenance`/`refuseIfForeignProvenance`: ONLY a
|
|
169
|
+
field Grafana explicitly reports as `""` or `"api"` is writable; a missing field refuses, with a
|
|
170
|
+
message that says so. Every resource has a test pinning all three cases (missing refuses, explicit
|
|
171
|
+
`""` is writable, `"file"` refuses).
|
|
172
|
+
|
|
173
|
+
**`Grafana.ContactPoint`:** no by-uid GET route exists (measured), so `fetchLive` lists every
|
|
174
|
+
contact point and matches `uid` client-side rather than narrowing by the renameable `name` label. A
|
|
175
|
+
secure `settings` key is never a literal prop — `secureSettingsRefs` names it, resolved from an env
|
|
176
|
+
var fresh at write time and merged into `settings` only there (`secret-refs.ts`, extracted from
|
|
177
|
+
`Grafana.Datasource`'s identical `secureJsonDataRefs` seam so both share one implementation).
|
|
178
|
+
`matches` excludes every `secureSettingsRefs` key from comparison entirely, since Grafana's plain
|
|
179
|
+
GET always redacts a secure field.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Alchemy changelog archive 4
|
|
2
|
+
|
|
3
|
+
[Current changelog](../../CHANGELOG.md) · [Archive index](./README.md)
|
|
4
|
+
|
|
5
|
+
**`Grafana.MuteTiming`:** a genuine SDK finding worth restating here — the generated
|
|
6
|
+
`MuteTimeInterval` type has no `provenance`/`version` fields, and its `TimeInterval` type is
|
|
7
|
+
missing the real Alertmanager time-interval fields (`weekdays`/`times`/`months`/etc.) entirely, even
|
|
8
|
+
though Grafana's real API sends and accepts all of them. Proven NOT to be a data-loss gap:
|
|
9
|
+
`@distilled.cloud/core`'s response decoding never runs a strict schema decode, only `JSON.parse`
|
|
10
|
+
plus a key-rename pass that leaves any key a type doesn't model verbatim — confirmed by driving the
|
|
11
|
+
real operation through this family's own fake-Grafana test harness. `mute-timing.ts` reads/writes
|
|
12
|
+
through a small locally-widened type instead of the SDK's own. Tracked in
|
|
13
|
+
`docs/upstream-conformance.md` as a real SDK type-generation gap (the same fix route as the other
|
|
14
|
+
named gaps there) — not attempted here, since `packages/distilled-grafana/src/` is vendored code
|
|
15
|
+
this kit never hand-edits.
|
|
16
|
+
|
|
17
|
+
**`Grafana.MessageTemplate`:** create and update are the same `PUT` wire call — measured, no
|
|
18
|
+
`routePostTemplate` exists — kept as two spec functions anyway so the shared engine's `diff` reports
|
|
19
|
+
a content change as `update`, not the `replace` it shows for any resource whose `spec.update` is
|
|
20
|
+
undefined.
|
|
21
|
+
|
|
22
|
+
Tests (`contact-point.test.ts` + `contact-point-secrets.test.ts` + `contact-point-provenance.test.ts`,
|
|
23
|
+
`mute-timing.test.ts`, `message-template.test.ts` — `ContactPoint`'s split three ways to stay under
|
|
24
|
+
the house's 250-line file cap) use the family's existing `fake-grafana.ts` harness and prove: a GET
|
|
25
|
+
never carries a body; a foreign-provenance object (explicit `"file"` or a MISSING field) refuses
|
|
26
|
+
update/destroy with no write sent, and an explicit `""` is writable; a transient failure (401/403)
|
|
27
|
+
propagates rather than folding to absent; an unchanged declaration is a noop despite an injected
|
|
28
|
+
field the declaration never mentioned; and, for `Grafana.ContactPoint`, that a secret value never
|
|
29
|
+
appears in a real `reconcile`'s returned attributes (not just in the declaration, which never held
|
|
30
|
+
it to begin with) — driven end to end through create, then the read-back `resource.ts` always does.
|
|
31
|
+
`subset-match.ts`'s array-length rule is flagged as unverified against a live mute timing and pinned
|
|
32
|
+
by its own test, per the same review.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- [#248](https://github.com/taslabs-net/homeflare-kit/pull/248) [`4681702`](https://github.com/taslabs-net/homeflare-kit/commit/46817022ef49186139287d8706be30cb57dbd742) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `@homeflare/alchemy/linux` now re-exports `renderContainerFile` and `containerPathFor` from
|
|
37
|
+
`Podman.Container`'s pure `.container` file renderer — the same kind of pure helper `renderUnit`/
|
|
38
|
+
`DEFAULT_UNIT_DIRECTORY` already are for `Systemd.Unit`. A consumer proving its own declared
|
|
39
|
+
props render into the directive set it expects (an equivalence/fixture test against a live host)
|
|
40
|
+
previously had no import path to the real renderer and had to reimplement the render contract by
|
|
41
|
+
hand to write that test at all (found: homeflare-ct100 PR [#1](https://github.com/taslabs-net/homeflare-kit/issues/1)).
|
|
42
|
+
|
|
43
|
+
## 0.33.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- [#245](https://github.com/taslabs-net/homeflare-kit/pull/245) [`672f60f`](https://github.com/taslabs-net/homeflare-kit/commit/672f60f8ad43b02b1dd151540a2d5f13e716e4f9) Thanks [@taslabs-net](https://github.com/taslabs-net)! - `grafana/*` ships `Grafana.Folder` and `Grafana.Dashboard`, on top of the same
|
|
48
|
+
`@distilled.cloud/grafana@0.2.0` (aliased onto `@homeflare/distilled-grafana`) operations that
|
|
49
|
+
fixed the family's SDK gap: `createFolder`/`getFolderByUID`/`updateFolder`/`deleteFolder` and
|
|
50
|
+
`postDashboard`/`getDashboard`/`deleteDashboard`, plus the typed `PreconditionFailed` (412) for
|
|
51
|
+
dashboard version conflicts. Same file shape and uid-required doctrine as the existing
|
|
52
|
+
`Grafana.Datasource` (`resource.ts`'s shared `GrafanaSpec`/`grafanaOperations`/`grafanaHandlers`,
|
|
53
|
+
`grafanaProviders(target)` for credentials). Full detail, including which claims were measured
|
|
54
|
+
against the SDK's generated types versus inferred (no live call was made in this PR):
|
|
55
|
+
`docs/grafana-folder-dashboard.md`.
|
|
56
|
+
|
|
57
|
+
**`Grafana.Folder`:** `description` is write-only — MEASURED against the SDK's generated types,
|
|
58
|
+
`Folder`'s GET response has no `description` field at all though create/update both accept one —
|
|
59
|
+
so it is sent on every write that declares it but never diffed, the same treatment
|
|
60
|
+
`Grafana.Datasource` gives `secureJsonDataRefs`. Nesting (`parentUid`) is create-only — MEASURED,
|
|
61
|
+
`UpdateFolderRequest` has no `parentUid` field — so a declaration whose `parentUid` no longer
|
|
62
|
+
matches the live folder REFUSES with `GrafanaFolderReparentError` instead of silently doing
|
|
63
|
+
nothing or deleting-and-recreating under the new parent. `update` sends `version` for optimistic
|
|
64
|
+
concurrency, but the SDK's own doc comment on `UpdateFolderRequest.version` says "only used by the
|
|
65
|
+
legacy folder implementation" — unverified whether Grafana's newer unified-storage folder backend
|
|
66
|
+
honors it at all, flagged rather than presented as a verified 412-style guard. Delete defaults to
|
|
67
|
+
`defaultRemovalPolicy: 'retain'`: `deleteFolder` takes every dashboard and alert rule in the
|
|
68
|
+
folder with it, the same multi-object-cascade class `openbao/mount.ts` already guards this way
|
|
69
|
+
(verified there against Alchemy's own `Apply.ts`, not re-verified here — same engine, same
|
|
70
|
+
option). A stack that wants the cascade opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
71
|
+
|
|
72
|
+
**`Grafana.Dashboard`:** the declared content is the dashboard JSON model, the same shape
|
|
73
|
+
Grafana's "Export as JSON" produces. `dashboard-model.ts`'s `normalizeModel` strips
|
|
74
|
+
`id`/`version`/`iteration` and forces `uid` to the resource's own value before any comparison, so
|
|
75
|
+
a pasted export and a freshly-read live model compare equal and an unchanged dashboard plans as a
|
|
76
|
+
true noop despite those fields moving on every live save. `panels` and each panel's `targets` are
|
|
77
|
+
array-order-significant and compared positionally; nothing in a dashboard model needed UniFi's
|
|
78
|
+
`sortedSet` treatment. **`matches` compares "declaration is a subset of live", not plain
|
|
79
|
+
equality** (`declaredContentMatches`) — an adversarial review of this PR found plain structural
|
|
80
|
+
equality made an ordinary, never-edited dashboard show `update` forever, because Grafana's own
|
|
81
|
+
schema migration on save decorates any panel/target whose `datasource` is absent with an explicit
|
|
82
|
+
reference; a declaration silent about a field is now tolerated rather than compared, while a field
|
|
83
|
+
it DOES mention is still caught exactly as before (traded off: an omitted field can no longer
|
|
84
|
+
force-clear one Grafana or a prior declaration set — see the doc for the full reasoning). Version
|
|
85
|
+
conflicts (412) fail loudly and are never overwritten blindly: `update` injects the version this
|
|
86
|
+
resource just read into the model it sends and never sets `overwrite`; a genuine conflict
|
|
87
|
+
propagates `PreconditionFailed` uncaught and fails the whole reconcile, with the next
|
|
88
|
+
plan/deploy's own `fetchLive` serving as the re-read. Delete does **not** default to retain — a
|
|
89
|
+
single-object blast radius, the same class `Grafana.Datasource` already accepts.
|
|
90
|
+
|
|
91
|
+
**Both refuse writes to a provisioned (file-managed) object** — `Folder.managedBy` (non-empty) and
|
|
92
|
+
`DashboardMeta.provisioned`/`provisionedExternalId`, checked in `update`/`destroy` before any SDK
|
|
93
|
+
call, naming the owning file when Grafana reports one. `matches` still compares structurally
|
|
94
|
+
regardless, so a mismatched declaration against a provisioned object shows `update` in a plan
|
|
95
|
+
rather than a silently-hidden noop, and only fails at the point reconcile would actually write;
|
|
96
|
+
adopting a provisioned object read-only (a declaration matching what is live) never reaches the
|
|
97
|
+
refusal. ⚠️ `Folder.managedBy`'s exact values are INFERRED from the SDK's schema, not measured —
|
|
98
|
+
no live instance in this house currently has a file-provisioned folder to read (`teslamate-grafana`
|
|
99
|
+
has none at all).
|
|
100
|
+
|
|
101
|
+
Tests (`folder.test.ts`, `dashboard.test.ts`, `dashboard-model.test.ts`) use the family's existing
|
|
102
|
+
`fake-grafana.ts` harness — the real distilled protocol, only wire responses faked — and prove:
|
|
103
|
+
every request carries only its own declared fields (a GET's body is asserted `undefined`, the
|
|
104
|
+
exact "distilled JSON-encodes unknown keys as a body" trap this family's shared memory names);
|
|
105
|
+
provisioned objects refuse update/destroy with no write sent; a transient failure (401/403)
|
|
106
|
+
propagates rather than folding to absent; an unchanged declaration is a noop despite volatile
|
|
107
|
+
fields differing; and a 412 propagates uncaught.
|
|
108
|
+
|
|
109
|
+
### Patch Changes
|
|
110
|
+
|
|
111
|
+
- [#246](https://github.com/taslabs-net/homeflare-kit/pull/246) [`953c361`](https://github.com/taslabs-net/homeflare-kit/commit/953c3617ac5b1cbd06d1c91f6562d2b4f3db346f) Thanks [@taslabs-net](https://github.com/taslabs-net)! - The `proxmox/*` family's `nodes/storage` sub-area, second resource (decision 43's serial proxmox
|
|
112
|
+
walk-down, after `access` in PR 231/239 and `storage` in PR 243): migrates `Proxmox.ZfsPool` off
|
|
113
|
+
`client.ts`'s hand-rolled `pve()`/`pveOperations` onto `@distilled.cloud/proxmox`'s typed
|
|
114
|
+
`nodes.getNodeDiskZfs`/`createNodeDiskZfs`/`deleteNodeDiskZfs`. Split into `zfs-pool.ts`
|
|
115
|
+
(resource + props), `zfs-pool-wire.ts` (the read side) and `zfs-pool-form.ts` (the write side) —
|
|
116
|
+
the api-token.ts/api-token-form.ts seam.
|
|
117
|
+
|
|
118
|
+
**Clears this family's own cries-wolf false updates.** `Proxmox.ZfsPool` reads with the `read`
|
|
119
|
+
role (`Sys.Audit` on `/` — unlike `Proxmox.Storage`/`Proxmox.Acl`, it never needed `provision` for
|
|
120
|
+
a GET), and the pre-migration `pveOperations.read` folded any REFUSED mint into "absent" the same
|
|
121
|
+
way every family did before its own fix — this family just hadn't been measured failing on the
|
|
122
|
+
agent lane yet, since its privilege requirement is looser. Now wired the same dual-path pattern
|
|
123
|
+
PR 239/243 established: `readPoolOrFail` (non-folding, used by `diff`, so a genuine transient
|
|
124
|
+
failure propagates and fails the plan loudly) and `readPool` (folding, used by `createPool`'s
|
|
125
|
+
settle-poll and by `read`/`reconcile`'s create-detection) — `read`'s own provider hook branches on
|
|
126
|
+
`output` from the start, so `alchemy drift` never reports a transient failure as a silent
|
|
127
|
+
`{action: 'missing'}` either.
|
|
128
|
+
|
|
129
|
+
**A missing pool is a 500, not a 404** — MEASURED against the live cluster (TB4 `n2`, read-role,
|
|
130
|
+
read-only probe): `GET /nodes/n2/disks/zfs/<missing>` answers a generic shelled-out `zpool status`
|
|
131
|
+
command failure at HTTP 500, the same class of measured 500 every other migrated family has.
|
|
132
|
+
|
|
133
|
+
**Decision 9's adopt-only mode is preserved exactly**, including the one nuance new to this
|
|
134
|
+
family: distilled's generated `CreateNodeDiskZfsRequest` marks `devices` and `raidlevel` as
|
|
135
|
+
REQUIRED fields (no `?`), unlike PVE's own runtime behaviour, which this resource's adopt-only
|
|
136
|
+
declarations depend on being able to omit. `createForm` stays a loosely-typed `Record<string,
|
|
137
|
+
string>` that can omit both — used for the vendor-constraint check (now `guardForm`, called
|
|
138
|
+
directly rather than through `guardWrite`, since this is the first migrated family whose
|
|
139
|
+
create-guard is CONDITIONAL: `guardWrite`'s own signature requires a real `EndpointKey`, but an
|
|
140
|
+
adopt-only declaration needs `undefined` some of the time) — and a separate `as unknown as` cast
|
|
141
|
+
builds the distilled-shaped request only at the point `createPool`'s own runtime guard has already
|
|
142
|
+
confirmed both fields are genuinely present, exactly preserving the pre-migration behaviour.
|
|
143
|
+
|
|
144
|
+
**One field needed distilled's hyphen-to-underscore rename** (`draid-config` -> `draid_config`,
|
|
145
|
+
the property `T.Body("draid-config")` maps back to the wire on the way out) — `storage.ts`/
|
|
146
|
+
`storage-form.ts`'s own measured finding from PR 243, here for a single field rather than a
|
|
147
|
+
free-form locator bag.
|
|
148
|
+
|
|
149
|
+
**`createPool`'s settle-poll (30 reads, 2s apart) is unchanged in behaviour** — still polling
|
|
150
|
+
`readPool`, not `@distilled.cloud/proxmox`'s own `Task.awaitTask`, which exists and would remove
|
|
151
|
+
the "not yet vs. forbidden" ambiguity the settle-poll's own comment already names as a known
|
|
152
|
+
weakness. Left as a documented follow-up rather than folded into this migration silently: swapping
|
|
153
|
+
it changes this resource's OBSERVABLE behaviour on a slow create (what a caller sees, and how),
|
|
154
|
+
which is an improvement to weigh on its own, not a transport swap.
|
|
155
|
+
|
|
156
|
+
**Two findings from adversarial review, both pre-existing (unchanged in shape from before this
|
|
157
|
+
migration, per `git blame`) but newly documented or fixed here:**
|
|
158
|
+
|
|
159
|
+
1. A re-run of `deploy` after `createPool`'s give-up `die` can, in a narrow window, POST a SECOND
|
|
160
|
+
`zpool create` at the same devices while the first worker is still writing them — the same
|
|
161
|
+
"not yet vs. forbidden" ambiguity the settle-poll already has, just for a retry rather than the
|
|
162
|
+
first attempt. Not fixed (a real fix needs task-status tracking, not a transport swap); the
|
|
163
|
+
give-up message and `createPool`'s own header now say so explicitly, where before they only
|
|
164
|
+
described the ambiguity as a slow-create observability gap.
|
|
165
|
+
2. `diff` (and its vendor-constraint check) never runs for a genuinely first-ever declaration —
|
|
166
|
+
upstream `Plan.ts` routes a brand-new resource straight to `create` without ever calling
|
|
167
|
+
`provider.diff` — so THIS WAS THE ONLY WRITE PATH IN THE FAMILY WITH NO VENDOR CHECK AT ALL,
|
|
168
|
+
before or after the migration started. Fixed: `createPool` now re-runs `guardForm` immediately
|
|
169
|
+
before its own POST, the same pattern `ceph-pool.ts`'s hand-written `reconcile` already uses
|
|
170
|
+
for the same reason.
|
|
171
|
+
|
|
172
|
+
**Also found and fixed while responding to that review** (not a finding from the review itself,
|
|
173
|
+
caught by re-running the full test suite after the changes above): an edit made to drop a dead
|
|
174
|
+
`matches()` export accidentally deleted the entire `live === undefined` branch of `diff` alongside
|
|
175
|
+
it, which would have made a genuinely-vanished pool plan `noop` forever instead of `update` —
|
|
176
|
+
caught immediately by `zfs-pool-adopt.test.ts`'s own pre-existing "an adopted pool that vanishes"
|
|
177
|
+
test failing, restored before this PR opened, left here for the record rather than silently
|
|
178
|
+
squashed into the diff.
|