Skip to content

Commit 793c301

Browse files
ResourceUID => ResourceID (and related fields)
1 parent 1c0baf5 commit 793c301

248 files changed

Lines changed: 2901 additions & 2951 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/kamu-sqlx-database-work/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,22 @@ pub struct MyEntityRow {
3434

3535
### Avoiding dynamic QueryBuilder in Postgres
3636

37-
For Postgres queries that filter on a set of composite keys (e.g. `(uid, kind)` pairs), prefer static `UNNEST`-based queries over dynamically built `OR` chains:
37+
For Postgres queries that filter on a set of composite keys (e.g. `(id, kind)` pairs), prefer static `UNNEST`-based queries over dynamically built `OR` chains:
3838

3939
```sql
4040
-- Prefer this:
41-
WHERE (resource_uid, resource_kind) IN (
41+
WHERE (resource_id, resource_kind) IN (
4242
SELECT * FROM UNNEST($1::uuid[], $2::text[])
4343
)
4444

4545
-- Over a dynamically built:
46-
-- WHERE (resource_uid = $1 AND resource_kind = $2) OR (resource_uid = $3 AND resource_kind = $4) ...
46+
-- WHERE (resource_id = $1 AND resource_kind = $2) OR (resource_id = $3 AND resource_kind = $4) ...
4747
```
4848

4949
Extract the arrays before entering any `async_stream::stream!` block so they are captured cleanly:
5050

5151
```rust
52-
let uids: Vec<uuid::Uuid> = queries.iter().map(|q| *q.uid.as_ref()).collect();
52+
let ids: Vec<uuid::Uuid> = queries.iter().map(|q| *q.id.as_ref()).collect();
5353
let kinds: Vec<String> = queries.iter().map(|q| q.kind.clone()).collect();
5454
```
5555

.agents/skills/kamu-test-harness/SKILL.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ harness struct. This keeps test code discoverable, namespaced, and self-containe
3838
async fn test_second_write_is_visible_across_accounts() {
3939
let harness = MyHarness::new();
4040

41-
harness.save_events("alice", uid, vec![created.clone()]).await;
42-
let prev_id = harness.get_last_event_id("alice", uid).await;
43-
harness.save_events_after("bob", uid, prev_id, vec![updated.clone()]).await;
41+
harness.save_events("alice", id, vec![created.clone()]).await;
42+
let prev_id = harness.get_last_event_id("alice", id).await;
43+
harness.save_events_after("bob", id, prev_id, vec![updated.clone()]).await;
4444

45-
let events = harness.get_events_for("alice", uid).await;
45+
let events = harness.get_events_for("alice", id).await;
4646
assert_eq!(events.len(), 2);
4747
}
4848
```
@@ -54,15 +54,15 @@ describes the scenario, not the plumbing.
5454

5555
```rust
5656
// Bad: free helper function — put it on the harness instead
57-
async fn save_and_get(svc: &FooService, uid: ResourceUID) -> Vec<Event> { ... }
57+
async fn save_and_get(svc: &FooService, id: ResourceID) -> Vec<Event> { ... }
5858

5959
// Bad: catalog / service resolution inside a test body
6060
let catalog = CatalogBuilder::new_chained(&harness.base_catalog).build();
6161
let svc = catalog.get_one::<FooService>().unwrap();
6262

6363
// Bad: stream boilerplate inside a test body
6464
let events = harness.bridge()
65-
.get_events(&uid, GetEventsOpts::default())
65+
.get_events(&id, GetEventsOpts::default())
6666
.map(|r| r.unwrap().1)
6767
.collect::<Vec<_>>()
6868
.await;
@@ -145,17 +145,17 @@ Each method unwraps internally; tests never see `Result` or stream chains.
145145

146146
```rust
147147
impl MyHarness {
148-
async fn get_events_for(&self, account: &str, uid: ResourceUID) -> Vec<TestEvent> {
148+
async fn get_events_for(&self, account: &str, id: ResourceID) -> Vec<TestEvent> {
149149
self.state_svc(account)
150-
.get_events(&uid, GetEventsOpts::default())
150+
.get_events(&id, GetEventsOpts::default())
151151
.map(|r| r.unwrap().1)
152152
.collect()
153153
.await
154154
}
155155

156-
async fn get_last_event_id(&self, account: &str, uid: ResourceUID) -> EventID {
156+
async fn get_last_event_id(&self, account: &str, id: ResourceID) -> EventID {
157157
self.state_svc(account)
158-
.get_events(&uid, GetEventsOpts::default())
158+
.get_events(&id, GetEventsOpts::default())
159159
.collect::<Vec<_>>()
160160
.await
161161
.into_iter()
@@ -168,24 +168,24 @@ impl MyHarness {
168168
async fn save_events(
169169
&self,
170170
account: &str,
171-
uid: ResourceUID,
171+
id: ResourceID,
172172
events: Vec<TestEvent>,
173173
) -> EventID {
174174
self.state_svc(account)
175-
.save_events(&uid, None, events)
175+
.save_events(&id, None, events)
176176
.await
177177
.unwrap()
178178
}
179179

180180
async fn save_events_after(
181181
&self,
182182
account: &str,
183-
uid: ResourceUID,
183+
id: ResourceID,
184184
prev_id: EventID,
185185
events: Vec<TestEvent>,
186186
) -> EventID {
187187
self.state_svc(account)
188-
.save_events(&uid, Some(prev_id), events)
188+
.save_events(&id, Some(prev_id), events)
189189
.await
190190
.unwrap()
191191
}

docs/internal/resources-framework.md

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
**One-paragraph mental model.** The resources framework is a generic, event-sourced,
1313
Kubernetes-inspired subsystem for *declarative* management of arbitrary "resource kinds". A user
1414
authors a **manifest** (`apiVersion` + `kind` + `headers` + `spec`); the framework stores it as an
15-
event-sourced aggregate, fills in server-owned headers (uid, timestamps, `generation`) and an
15+
event-sourced aggregate, fills in server-owned headers (id, timestamps, `generation`) and an
1616
**initial** server-owned `status`, then **asynchronously reconciles** it toward the desired state
1717
(the status progresses `Pending → Reconciling → Ready`/`Failed`). Every kind (today:
1818
`VariableSet`, `SecretSet`) implements a small set of traits and registers a **dispatcher** keyed by
@@ -119,7 +119,7 @@ long-term goal. This page documents what exists now.
119119

120120
| Term | Meaning |
121121
| --- | --- |
122-
| **Resource** | A single managed object instance of a given kind, identified by a `ResourceUID`. |
122+
| **Resource** | A single managed object instance of a given kind, identified by a `ResourceID`. |
123123
| **Kind** | The resource type discriminator string, e.g. `VariableSet`, `SecretSet`. |
124124
| **ApiVersion** | Versioning string for the kind's schema, e.g. `kamu.dev/v1alpha1`. |
125125
| **Descriptor** | The `(kind, apiVersion)` pair (`ResourceDescriptor`) used to route to the right dispatcher. |
@@ -143,7 +143,7 @@ long-term goal. This page documents what exists now.
143143
The rules below hold across the framework. They are the contract a maintainer (or coding agent) must
144144
not break; most are enforced in code and exercised by tests — pointers given where useful.
145145

146-
- **`ResourceUID` is immutable and server-allocated.** A new resource's UID comes from
146+
- **`ResourceID` is immutable and server-allocated.** A new resource's UID comes from
147147
`GenericResourceQueryService::allocate_uid()`; callers cannot choose it. Once assigned it never
148148
changes (it is the primary key — see [§6](#6-persistence-model)).
149149
- **`(account_id, kind, name)` is unique.** Enforced by a DB unique constraint
@@ -249,7 +249,7 @@ pub trait DeclarativeResource:
249249
+ TryFrom<ResourceSnapshot, Error = InternalError>
250250
+ From<Self>;
251251

252-
fn uid(&self) -> &ResourceUID;
252+
fn id(&self) -> &ResourceID;
253253
fn headers(&self) -> &ResourceHeaders;
254254
fn spec(&self) -> &Self::Spec;
255255
fn status(&self) -> &Self::Status;
@@ -269,7 +269,7 @@ pub trait ReconcilableResource: DeclarativeResource {
269269

270270
fn needs_reconciliation(&self) -> bool { /* observed_generation vs generation */ }
271271

272-
fn try_create(now, uid, headers: ResourceHeadersInput, spec) -> Result<Self, LifecycleError>;
272+
fn try_create(now, id, headers: ResourceHeadersInput, spec) -> Result<Self, LifecycleError>;
273273
fn try_update_headers(&mut self, now, new_headers: ResourceHeadersInput) -> ...;
274274
fn try_update_spec(&mut self, now, new_spec: Self::Spec) -> ...;
275275
fn try_delete(&mut self, now, tombstone_name: String) -> ...;
@@ -301,7 +301,7 @@ current state.
301301
### Repository
302302

303303
`ResourceRepository` (`repo/`) is the persistence seam: allocate UID, create/update snapshot
304-
(with optimistic `expected_last_event_id`), find by name/uid, search identities, and stream UIDs /
304+
(with optimistic `expected_last_event_id`), find by name/id, search identities, and stream UIDs /
305305
snapshots by kind.
306306

307307
### Dispatchers
@@ -338,7 +338,7 @@ pub struct ResourceManifest {
338338

339339
#[serde(deny_unknown_fields)] // ← unknown fields (e.g. `status`) are rejected
340340
pub struct ResourceManifestHeaders {
341-
pub uid: Option<ResourceUID>, // optional — NOT assignable; an exact pointer to an
341+
pub id: Option<ResourceID>, // optional — NOT assignable; an exact pointer to an
342342
// existing resource for updates (e.g. when renaming)
343343
pub account: Option<ResourceManifestAccount>, // optional — by name OR id; defaults to caller
344344
pub name: ResourceName, // required
@@ -348,13 +348,13 @@ pub struct ResourceManifestHeaders {
348348
}
349349
```
350350

351-
A user may write **only**: `apiVersion`, `kind`, `headers.{uid?, account?, name, description?,
351+
A user may write **only**: `apiVersion`, `kind`, `headers.{id?, account?, name, description?,
352352
labels, annotations}`, and `spec`. `deny_unknown_fields` means a manifest **cannot** carry `status`,
353353
timestamps, or `generation` — those are server-owned.
354354

355-
The `uid` is **not** something the user assigns — a new resource's UID is always allocated by the
355+
The `id` is **not** something the user assigns — a new resource's UID is always allocated by the
356356
server. It may only be *supplied* on a manifest to point at an already-existing resource for an
357-
update; this is what lets a resource be renamed (the `uid` keeps the identity stable while `name`
357+
update; this is what lets a resource be renamed (the `id` keeps the identity stable while `name`
358358
changes). Omit it for normal create/update-by-name.
359359

360360
**(2) Framework-generated — the rest of headers + all of status.**
@@ -381,18 +381,18 @@ pub struct ResourceStatus { // entirely server-owned
381381
}
382382
```
383383

384-
Also generated: `uid` (allocated if the manifest omitted it) and `last_reconciled_at`.
384+
Also generated: `id` (allocated if the manifest omitted it) and `last_reconciled_at`.
385385

386386
**(3) Persisted form — the snapshot.** `ResourceSnapshot`
387387
([`core/resource_snapshot.rs`](/src/domain/resources/domain/src/core/resource_snapshot.rs))
388388
combines authored + generated + event-sourcing bookkeeping:
389389

390390
```rust
391391
pub struct ResourceSnapshot {
392-
pub uid: ResourceUID,
392+
pub id: ResourceID,
393393
pub kind: String,
394394
pub api_version: String,
395-
pub headers: ResourceHeaders, // authored fields + generated fields
395+
pub headers: ResourceHeaders, // authored fields + generated fields
396396
pub spec: serde_json::Value, // authored (may be transformed — see SecretSet)
397397
pub status: Option<serde_json::Value>, // generated
398398
pub last_reconciled_at: Option<DateTime<Utc>>, // generated
@@ -402,9 +402,9 @@ pub struct ResourceSnapshot {
402402

403403
```mermaid
404404
flowchart LR
405-
M["Manifest (authored)<br/>apiVersion, kind<br/>headers: name, account?, uid?,<br/>description?, labels, annotations<br/>spec"]
406-
A["apply use case<br/>(resolve account, allocate uid,<br/>bump generation, set timestamps)"]
407-
S["Snapshot / State<br/>= authored fields<br/>+ <b>generated</b>: account(ID), uid,<br/>generation, created/updated/deleted_at,<br/>status{phase, observedGeneration, conditions}"]
405+
M["Manifest (authored)<br/>apiVersion, kind<br/>headers: name, account?, id?,<br/>description?, labels, annotations<br/>spec"]
406+
A["apply use case<br/>(resolve account, allocate id,<br/>bump generation, set timestamps)"]
407+
S["Snapshot / State<br/>= authored fields<br/>+ <b>generated</b>: account(ID), id,<br/>generation, created/updated/deleted_at,<br/>status{phase, observedGeneration, conditions}"]
408408
M --> A --> S
409409
```
410410

@@ -428,14 +428,14 @@ Resources are **event-sourced with a materialized snapshot per resource**. Stora
428428

429429
**Two tables:**
430430

431-
- **`resources`** — one row per resource (the snapshot): `resource_uid` (UUID, **PK**),
431+
- **`resources`** — one row per resource (the snapshot): `resource_id` (UUID, **PK**),
432432
`account_id`, `resource_kind`, `api_version`, `resource_name`, `description`, `labels`/`annotations`
433433
(JSONB), `spec` (JSONB), `status` (JSONB, nullable), `generation`, `created_at`/`updated_at`,
434434
`deleted_at` (nullable), `last_reconciled_at`, `last_event_id`. **Uniqueness:**
435435
`UNIQUE (account_id, resource_kind, resource_name)` — note **api_version is not part of the key**.
436436
A partial index on `(account_id, kind, api_version, status->>'phase') WHERE deleted_at IS NULL`
437437
backs the summary projection.
438-
- **`resource_events`** — append-only log: `event_id` (BIGINT from a sequence, PK), `resource_uid`
438+
- **`resource_events`** — append-only log: `event_id` (BIGINT from a sequence, PK), `resource_id`
439439
(FK → `resources`), `resource_kind`, `event_time`, `event_type`, `event_payload` (JSONB).
440440

441441
**Source of truth.** The event log is authoritative — aggregates are rebuilt by projecting events
@@ -487,7 +487,7 @@ Resolution + permission checks live in `ResourceAccountResolverImpl`
487487
(`CurrentAccountSubject`) as the caller; the same admin rule applies for cross-account targeting.
488488
- **UID belonging to a different account.** Lookups are account-scoped (`find_account_snapshot`
489489
filters by `account_id`). A UID that exists but belongs to another account is reported as
490-
**not-found** (`ResourceUIDNotFoundError`), not "forbidden" — so existence is not leaked across
490+
**not-found** (`ResourceIDNotFoundError`), not "forbidden" — so existence is not leaked across
491491
accounts. A UID of the wrong *kind* yields `ResourceTypeMismatchError`.
492492
- **Account-deletion cascade.** When an account is deleted, `DeleteAccountResourcesUseCase` deletes
493493
that account's resources (see [§13](#13-data-flow-walkthroughs)). It operates on live resources;
@@ -501,19 +501,19 @@ Resolution + permission checks live in `ResourceAccountResolverImpl`
501501
The apply planner ([`services/apply_resource_planner.rs`](/src/domain/resources/services/src/services/apply_resource_planner.rs))
502502
decides create vs update by resolving the target resource first:
503503

504-
- **No `uid` in manifest** → resolve by `(account, kind, name)`. Found → update; not found → create
504+
- **No `id` in manifest** → resolve by `(account, kind, name)`. Found → update; not found → create
505505
(new UID allocated).
506-
- **`uid` in manifest** → load that exact resource (the "exact pointer"). This is what enables a
507-
**rename**: supply the `uid` and a new `headers.name`; identity stays stable while the name
506+
- **`id` in manifest** → load that exact resource (the "exact pointer"). This is what enables a
507+
**rename**: supply the `id` and a new `headers.name`; identity stays stable while the name
508508
changes.
509509

510510
Concrete conflict cases:
511511

512512
| Case | Outcome |
513513
| --- | --- |
514-
| `uid` + changed `headers.name` | **Rename** — name updated on the same resource (`Update`). |
515-
| `uid` whose resource is a different `kind` (or apiVersion type) | **Reject**`ResourceTypeMismatchError`. |
516-
| `uid` resolving to a resource in a different account | **Not found**`ResourceUIDNotFoundError` (account-scoped lookup; existence not leaked). |
514+
| `id` + changed `headers.name` | **Rename** — name updated on the same resource (`Update`). |
515+
| `id` whose resource is a different `kind` (or apiVersion type) | **Reject**`ResourceTypeMismatchError`. |
516+
| `id` resolving to a resource in a different account | **Not found**`ResourceIDNotFoundError` (account-scoped lookup; existence not leaked). |
517517
| `headers.account` targeting another account without admin | **Reject**`AccessError::Unauthorized` ([§7](#7-account-resolution--authorization)). |
518518
| Rename target name already taken (same account+kind) | **Reject** — unique-constraint `Duplicate` at persistence. |
519519
| Update by name where another account has the same name | **No conflict** — the account disambiguates; each `(account, kind, name)` is independent. |
@@ -612,7 +612,7 @@ pub trait ResourceFacade: Send + Sync {
612612

613613
async fn plan_apply_manifest(&self, request: ApplyManifestRequest) -> Result<ApplyManifestPlanningDecision, ...>;
614614
async fn apply_manifest(&self, request: ApplyManifestRequest) -> Result<ApplyManifestApplicationDecision, ...>;
615-
async fn delete(&self, selector: ResourceSelector) -> Result<ResourceUID, ...>;
615+
async fn delete(&self, selector: ResourceSelector) -> Result<ResourceID, ...>;
616616
async fn delete_many(&self, selector: ResourceBatchSelector) -> ...;
617617
}
618618
```
@@ -622,7 +622,7 @@ pub trait ResourceFacade: Send + Sync {
622622
```rust
623623
pub struct ResourceSelector { pub account: Option<ResourceManifestAccount>, pub kind: String,
624624
pub api_version: Option<String>, pub resource_ref: ResourceRef }
625-
pub enum ResourceRef { ById(ResourceUID), ByName(ResourceName) }
625+
pub enum ResourceRef { ById(ResourceID), ByName(ResourceName) }
626626
pub enum ResourceManifestFormat { Json, Yaml }
627627
pub enum SpecViewMode { Encrypted /* default */, Revealed }
628628
```
@@ -798,7 +798,7 @@ sequenceDiagram
798798
participant ST as Event store + repo
799799
participant OB as Outbox
800800
801-
UC->>L: load(uid) %% needs_reconciliation()? observed_generation < generation
801+
UC->>L: load(id) %% needs_reconciliation()? observed_generation < generation
802802
UC->>ST: try_mark_reconciliation_started → append event
803803
UC->>Rec: reconcile(resource) %% e.g. SecretSet builds encrypted projection
804804
alt success
@@ -856,7 +856,7 @@ flowchart LR
856856
A["apply use case<br/>(persist + produce Applied)"] --> OB[("Outbox")]
857857
OB --> C["ResourceLifecycleMessageConsumer<br/>(event bridge)"]
858858
C -->|lookup by kind/apiVersion| D["per-kind ResourceLifecycleEventDispatcher<br/>handle_applied()"]
859-
D --> R["ReconcileResourceUseCase::execute(uid)"]
859+
D --> R["ReconcileResourceUseCase::execute(id)"]
860860
R --> Rec["Reconciler&lt;R&gt;"]
861861
```
862862

@@ -865,7 +865,7 @@ flowchart LR
865865
`MESSAGE_CONSUMER_KAMU_RESOURCE_LIFECYCLE_EVENT_BRIDGE`,
866866
[`message_handlers/resource_lifecycle_message_consumer.rs`](/src/domain/resources/services/src/message_handlers/resource_lifecycle_message_consumer.rs))
867867
consumes it and looks up the per-kind `ResourceLifecycleEventDispatcher` by descriptor.
868-
3. That dispatcher's `handle_applied` calls **`ReconcileResourceUseCase::execute(uid)`**, which loads
868+
3. That dispatcher's `handle_applied` calls **`ReconcileResourceUseCase::execute(id)`**, which loads
869869
the aggregate, marks reconciliation started, invokes the kind's `Reconciler<R>`, and records the
870870
outcome (producing `ReconciliationSucceeded` / `ReconciliationFailed`).
871871

@@ -1053,11 +1053,11 @@ list/search, supported kinds, get-identity, error taxonomy, delete, render-manif
10531053
summary, and spec view modes.
10541054

10551055
**E2E (CLI)**
1056-
[`src/e2e/app/cli/repo-tests/src/commands/resources`](/src/e2e/app/cli/repo-tests/src/commands/resources).
1056+
[`src/e2e/app/cli/repo-tests/src/commands/resources`](/src/e2e/app/cli/repo-tests/src/commands/resources/).
10571057
Drive the real CLI binary via `KamuCliPuppet`. A `ResourceCtx` abstraction
10581058
(`repo-tests/src/resources/context.rs`) runs every scenario against **both** an implicit local
10591059
context and a registered remote context. The `get_view.rs` helper parses `get -o json` into a
1060-
queryable `ResourceView` (`ident()`, `variable()`, `has_secret()`, `uid()`, …) so assertions are
1060+
queryable `ResourceView` (`ident()`, `variable()`, `has_secret()`, `id()`, …) so assertions are
10611061
targeted rather than brittle; a golden-view test pins the whole-document shape once per kind.
10621062

10631063
### Testing policy — what belongs where

0 commit comments

Comments
 (0)