All checks were successful
Run Check Script / check (pull_request) Successful in 1m55s
126 lines
4.8 KiB
Markdown
126 lines
4.8 KiB
Markdown
# Typed Score references
|
|
|
|
Harmony builds loosely dependent but strongly typed infrastructure components
|
|
using Refs.
|
|
|
|
This means that a `ZitadelScore` does not directly depend on a
|
|
`PostgreSQLScore`. It can instead accept a `PostgreSQLRootAccountRef` in its
|
|
builder to know how to connect to PostgreSQL. This is safer than passing strings
|
|
around: the Ref has a type Rust can check, and consumers do not need to parse or
|
|
reconstruct the producer's naming conventions.
|
|
|
|
The loose dependency matters. Previous design proposals treated composition as a
|
|
dependency graph or pipeline, which forced components to know too much about one
|
|
another. A Ref is only a typed reference to declarative desired state. It does
|
|
not give the consumer control over the producer's lifecycle.
|
|
|
|
Most importantly, a Ref does not guarantee that its target exists or has
|
|
converged when it is passed. Like a Kubernetes reference, it is eventually
|
|
consistent. Consumers must tolerate the target being absent, starting, or
|
|
temporarily unavailable.
|
|
|
|
Kubernetes gets this right: infrastructure is chaotic. A component can fail
|
|
after it becomes ready. Drives fail, power goes out, and networks split. The
|
|
long-term answer is continuous convergence, not an assumption that deployment
|
|
finishes infrastructure forever.
|
|
|
|
The convergence mechanism is outside ADR-028. Choosing eventually consistent
|
|
Refs now lets Scores declare relationships without committing Harmony to a
|
|
scheduler, dependency engine, or reconciliation design.
|
|
|
|
## The whole pattern
|
|
|
|
[](../diagrams/score-ref-composition.svg)
|
|
|
|
```rust
|
|
pub struct DbRef {
|
|
// Desired-state properties needed to connect.
|
|
}
|
|
|
|
let db_score = SomeDbScore::new(...);
|
|
let some_db_ref = db_score.connection();
|
|
let app_score = AppScore::new(...).with_db(&some_db_ref);
|
|
```
|
|
|
|
That is the mechanism. `connection()` derives `DbRef` from the database's desired
|
|
state. It performs no I/O and does not require `SomeDbScore` to have been
|
|
interpreted.
|
|
|
|
`AppScore` knows `DbRef`. It does not need to know `SomeDbScore`, its provider,
|
|
or its readiness procedure.
|
|
|
|
Compare that with copying a connection string:
|
|
|
|
```rust
|
|
app.database_url("postgres://some-db.apps.svc:5432/app");
|
|
```
|
|
|
|
That string duplicates database naming and connection knowledge in the consumer.
|
|
A rename or layout change can leave valid Rust that fails at runtime. A typed
|
|
Ref removes that string contract and makes type-level contract changes visible
|
|
to the compiler.
|
|
|
|
## Cyclic relationships
|
|
|
|
A cycle is the same pattern in both directions:
|
|
|
|
[](../diagrams/score-ref-cycle.svg)
|
|
|
|
```rust
|
|
let broker_score = BrokerScore::new(...);
|
|
let broker_ref = broker_score.connection();
|
|
|
|
let auth_score = AuthScore::new(...).with_broker(&broker_ref);
|
|
let auth_ref = auth_score.endpoint();
|
|
|
|
let broker_score = broker_score.with_auth(&auth_ref);
|
|
```
|
|
|
|
No Score has run. Each Ref comes from desired state, so the two relationships
|
|
can be declared without traversing a graph or resolving either resource.
|
|
|
|
## Eventual consistency
|
|
|
|
Composition code passes Refs; it does not wait for them to become ready. The
|
|
consumer's `Interpret`/`Topology` path handles an unresolved target in the way
|
|
appropriate for that component. It may:
|
|
|
|
- emit a native Kubernetes reference and let Kubernetes reconcile it;
|
|
- wait or retry when the value is needed immediately;
|
|
- return a non-success outcome for the current interpretation attempt.
|
|
|
|
ADR-028 does not choose among those behaviors or define when another attempt
|
|
runs. It only requires consumers not to treat possession of a Ref as proof of
|
|
availability.
|
|
|
|
Refs may also identify values generated later, such as an OIDC client ID. The
|
|
same rule applies: passing the Ref does not claim that the value exists yet.
|
|
|
|
## What types guarantee
|
|
|
|
Concrete types such as `PostgreSQLRootAccountRef`, `OidcApplicationRef`, and
|
|
`NatsAccountRef` prevent unrelated references from being exchanged by accident.
|
|
They also remove string parsing and copied naming rules from consumers.
|
|
|
|
Types do not prove that:
|
|
|
|
- the referenced resource exists;
|
|
- the producer has converged;
|
|
- two live systems can communicate;
|
|
- credentials are authorized;
|
|
- the infrastructure will remain available.
|
|
|
|
Those remain runtime concerns.
|
|
|
|
## Authoring rule
|
|
|
|
Use a Ref when one Score would otherwise copy a name, URL, ID, credential
|
|
location, or other desired-state identifier owned by another component. Use an
|
|
ordinary typed value when there is no such relationship.
|
|
|
|
Refs express relationships. They do not impose execution order, create a
|
|
pipeline, or make Harmony responsible for continuous convergence.
|
|
|
|
See [ADR-028](../adr/028-typed-score-references.md) for the decision and the
|
|
[Fleet reference](../reference/fleet-score-references.md) for the first application.
|