Documentation
¶
Overview ¶
Package typeset is GitOps Reverser's single decision surface for "is this resource type followable, and if not, what is the one reason it is not?".
It is the greenfield model from docs/spec/type-followability.md. Every served type carries one TypeRecord with one Followability: a verdict, a one-line summary, and the full funnel-ordered list of requirement checks. That single value replaces the old split between a "requirements" table and a "health conditions" table — the failing check is the explanation.
The package is deliberately a leaf: it depends only on apimachinery schema, never on a Kubernetes client, controller runtime, or the watch manager. That lets both the live cluster path (internal/watch) and the no-cluster manifest analyzer share one decision surface and one reason-code vocabulary.
Index ¶
- Constants
- type Check
- type Confidence
- type Entry
- type EventKind
- type Followability
- type Identity
- type LifecycleEvent
- type Lookup
- type Observation
- type Observer
- type Origin
- type OriginKind
- type Reason
- type Registry
- func (r *Registry) All() []TypeRecord
- func (r *Registry) ByGVK(gvk schema.GroupVersionKind) (TypeRecord, bool)
- func (r *Registry) ByGVR(gvr schema.GroupVersionResource) (TypeRecord, bool)
- func (r *Registry) ByGroupResource(group, resource string) []TypeRecord
- func (r *Registry) Followable() []TypeRecord
- func (r *Registry) Generation() uint64
- func (r *Registry) Ready() bool
- func (r *Registry) Revision() uint64
- func (r *Registry) Subscribe(obs Observer)
- func (r *Registry) Update(observations []Observation, generation uint64)
- func (r *Registry) UpdateFromScan(scan Scan)
- type Requirement
- type Result
- type ScaleBinding
- type Scan
- type Scope
- type Snapshot
- type StatusFact
- type Subresources
- type TypeRecord
- type Verdict
Constants ¶
const RemovalGrace = 60 * time.Second
RemovalGrace is how long a previously-live type that stops being observed is held as retained before it leaves the live set. It is product safety, not tuning, so it is a fixed constant: it stops a short discovery blink from turning into a large Git sweep. See the "Live set and the 60-second grace" section of the design.
const ScaleSourceBuiltinRegistry = "builtin-registry"
ScaleSourceBuiltinRegistry labels a ScaleBinding resolved from the built-in registry below, so it reads identically to a CRD-sourced binding for the writer.
const SettleWindow = 5 * time.Second
SettleWindow is how long a type must stay continuously followable before the registry emits a TypeActivated for it. It governs ACTIVATION, not removal: a flapping or just-appeared type does not drive a per-type reconcile on a state that is about to change again. Like RemovalGrace it is product safety, not tuning, so it is a fixed constant — but it is deliberately short where the grace is long. See docs/spec/type-lifecycle-events-and-wobble-settling.md (Proposal 2).
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Check ¶
type Check struct {
Requirement Requirement
Result Result
Reason Reason // empty on pass/skip; otherwise the single reason code
Detail string // bounded human detail, e.g. "patch"
}
Check is one requirement's evaluated result in the funnel.
type Confidence ¶
type Confidence string
Confidence records how strongly the origin classification is held: observed from direct CRD/APIService evidence, inferred from the group/version alone, or unknown.
const ( // ConfidenceObserved is backed by direct evidence (a CRD or APIService object). ConfidenceObserved Confidence = "observed" // ConfidenceInferred is derived from the group/version shape, without an object. ConfidenceInferred Confidence = "inferred" // ConfidenceUnknown is no basis for classification. ConfidenceUnknown Confidence = "unknown" )
type Entry ¶
type Entry struct {
GVK schema.GroupVersionKind
GVR schema.GroupVersionResource
Namespaced bool
Verbs []string
Preferred bool
Subresource bool
Allowed bool // product policy permits mirroring this resource
PolicyReason string // why it is not allowed, when Allowed is false
Degraded bool // the backing group/version is currently degraded
// Sensitive reports whether this resource must use the encrypted Git write path.
// It is a startup-known policy fact, applied by the entry builder (the catalog
// applies the configured SensitiveResourcePolicy), not inferred inside typeset.
Sensitive bool
}
Entry is one served API resource's raw facts — the neutral input to the scan. Both the live discovery catalog and a serialized snapshot convert their resources to this shape, so observation-building (identity uniqueness, origin, scale, policy) lives in exactly one place and the live and fixture paths agree on every verdict.
type EventKind ¶
type EventKind string
EventKind names a per-type lifecycle transition the registry emits. The events are transitions between the existing verdicts (no new verdict vocabulary): the registry is the single component that owns the decision, so it computes the transition once and names it, instead of every consumer re-detecting the same edge by diffing tables.
const ( // TypeActivated fires when a type has been continuously Followable for the settle // window: it is healthy and stable, so its stream may be opened or re-anchored. TypeActivated EventKind = "TypeActivated" // TypeWobbling fires on Followable -> Retained: a transient unserved blip. Do NOT sweep; // hold the type's streams and files until it settles or drops. TypeWobbling EventKind = "TypeWobbling" // TypeRecovered fires on Retained -> Followable: the wobble resolved. It collapses into // a fresh TypeActivated once the settle window elapses again. TypeRecovered EventKind = "TypeRecovered" // TypeRemoved fires when a previously-live type leaves the live set because its removal // grace elapsed (absence-expired): it is genuinely gone, so untracking is scoped to THIS // type only. TypeRemoved EventKind = "TypeRemoved" // TypeRefused fires when a previously-live type fails a permanent check: never watch it, // stop its streams, surface it in status. TypeRefused EventKind = "TypeRefused" )
type Followability ¶
type Followability struct {
Verdict Verdict
Summary string // one line, e.g. "not followable — missing required verb: patch"
Checks []Check
}
Followability answers "can I act on this, and why not?" in one value.
func Evaluate ¶
func Evaluate(obs Observation) Followability
Evaluate reduces one Observation into its Followability: every requirement check in funnel order, plus the mechanical verdict and one-line summary. It is the single decision point — there is no second "inspect" pass.
func (Followability) Check ¶
func (f Followability) Check(req Requirement) (Check, bool)
Check returns the evaluated check for a requirement, if present.
func (Followability) FirstFailure ¶
func (f Followability) FirstFailure() (Check, bool)
FirstFailure returns the first failed check in funnel order, if any.
type Identity ¶
type Identity struct {
GVK schema.GroupVersionKind
GVR schema.GroupVersionResource
Scope Scope
}
Identity is the one true name of a type. For a followable type the GVK <-> GVR bijection is closed, so GVK and GVR always round-trip.
type LifecycleEvent ¶
type LifecycleEvent struct {
Kind EventKind
GVK schema.GroupVersionKind
GVR schema.GroupVersionResource
From Verdict
To Verdict
Reason Reason
Generation uint64
At time.Time
}
LifecycleEvent is one named transition between verdicts for a single type. It carries the identity, the verdicts it crossed, the single machine-readable reason for a failure, the scan generation the transition was computed at, and the time it was observed.
type Lookup ¶
type Lookup interface {
Ready() bool
ByGVK(gvk schema.GroupVersionKind) (TypeRecord, bool)
}
Lookup is the minimal followability surface every consumer reads: "is this type followable, and what is its resolved identity?". It replaces the old mapping.ResourceMapper contract — there is one notion of followable, and callers gate on TypeRecord.Followable() rather than interpreting a status vocabulary.
Ready reports whether the backing scan holds trusted data. A not-ready Lookup is the "structure-only / no API source" mode: it cannot judge followability, so a consumer must not draw a watched/unwatched (or destructive) conclusion from it.
type Observation ¶
type Observation struct {
Identity Identity
Origin Origin
Preferred bool
Verbs []string
Subresources Subresources
// served / trusted / stable facts.
Served bool // discovery currently serves this as a top-level resource
SubresourceOnly bool // the kind is served only as a subresource
Trusted bool // backing group/version came from trusted, non-degraded discovery
CatalogReady bool // the catalog has accepted any trusted discovery data
AbsenceExpired bool // the type is mid-disappearance and the removal grace has elapsed
// identity facts.
GVKUnique bool // exactly one GVR serves this GVK
GVRUnique bool // this GVR resolves back to exactly one Kind
GVKConflictDetail string // e.g. "widgets, widgetz" when GVKUnique is false
GVRConflictDetail string // e.g. "Widget, Gadget" when GVRUnique is false
// policy facts (computed by the registry from group/resource).
Denied bool
DenyDetail string
Sensitive bool
SensitiveSupported bool
}
Observation is the raw per-type facts the funnel reduces into a Followability. It is built by the scan (discovery + CRD/APIService evidence + the built-in scale registry + product policy) and carries every fact a check needs, so Evaluate is a pure function with no side inputs. The registry owns how observations are built; the funnel owns only how they are judged.
func ObservationsFromEntries ¶
func ObservationsFromEntries(entries []Entry, catalogReady bool) []Observation
ObservationsFromEntries projects served entries into one Observation per top-level type — the "Scan -> Observation" reduction. Subresources are folded into their parent's record, never emitted as their own observation. catalogReady reports whether the backing scan holds trusted data, feeding the trusted requirement's catalog-unavailable distinction.
type Observer ¶
type Observer func(LifecycleEvent)
Observer receives lifecycle events from the registry. It is invoked by Update after the new records are published, in generation order and serialized with other updates, so an observer may read the registry but must NOT block the updater (a real consumer enqueues the event and returns). See Registry.Subscribe.
type Origin ¶
type Origin struct {
Kind OriginKind
Confidence Confidence
// Evidence is bounded human detail, e.g. crontabs.stable.example.com.
Evidence string
}
Origin is the provenance of a served type plus how strongly it is held.
type OriginKind ¶
type OriginKind string
OriginKind classifies where a served type comes from.
const ( // OriginBuiltin is a core or built-in Kubernetes API group/version. OriginBuiltin OriginKind = "builtin" // OriginCRD is a type backed by a CustomResourceDefinition. OriginCRD OriginKind = "crd" // OriginAggregated is a type served by an aggregated API server (APIService). OriginAggregated OriginKind = "aggregated" // OriginUnknown is a served type the scan could not classify; it fails the // origin requirement. OriginUnknown OriginKind = "unknown" )
type Reason ¶
type Reason string
Reason is the single machine-readable cause of a failed check. It is the one vocabulary used everywhere a type is turned away — lookups, the live-set report, and operator status — so "why isn't this picked up?" always has the same answer.
const ( // ReasonNotServed — trusted discovery has no top-level resource for this kind. ReasonNotServed Reason = "not-served" // ReasonSubresourceOnly — the kind is served only as a subresource. ReasonSubresourceOnly Reason = "subresource-only" // ReasonDiscoveryDegraded — discovery currently fails for the backing group/version. ReasonDiscoveryDegraded Reason = "discovery-degraded" ReasonCatalogUnavailable Reason = "catalog-unavailable" // ReasonAbsenceExpired — the type disappeared and the removal grace has elapsed. ReasonAbsenceExpired Reason = "absence-expired" // ReasonGVKNotUnique — the GVK is served by more than one GVR. ReasonGVKNotUnique Reason = "gvk-not-unique" // ReasonGVRNotUnique — the GVR resolves to more than one Kind. ReasonGVRNotUnique Reason = "gvr-not-unique" // ReasonScopeUnknown — discovery did not establish a namespaced/cluster scope. ReasonScopeUnknown Reason = "scope-unknown" // ReasonMissingVerb — discovery does not advertise a required verb (Detail names it). ReasonMissingVerb Reason = "missing-verb" // ReasonOriginUnknown — the served type could not be classified. ReasonOriginUnknown Reason = "origin-unknown" // ReasonDeniedByPolicy — product policy refuses to mirror this type. ReasonDeniedByPolicy Reason = "denied-by-policy" // ReasonSensitiveUnsupported — sensitive type without supported write handling. ReasonSensitiveUnsupported Reason = "sensitive-unsupported" // ReasonScalePathUnresolved — scale is used but the parent replica path is unknown. ReasonScalePathUnresolved Reason = "scale-path-unresolved" )
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the single decision surface: it turns observations plus the live-set grace into one TypeRecord per known type, and answers the lookups every consumer reads. It owns identity, the live set, and the removal grace; consumers never recompute followability, they read it.
Additions are fast and removals are slow: a newly observed followable type enters the live set immediately, while a previously-live type that stops being observed is held as retained for RemovalGrace before it drops. The clock is injectable so the grace is deterministic in tests.
Registry is safe for concurrent readers and a single updater.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry builds an empty registry with the fixed removal grace and a real clock.
func NewSnapshotRegistry ¶
NewSnapshotRegistry builds a Registry from a Snapshot. A NotReady snapshot yields an unpublished (structure-only) registry; otherwise the entries are projected into observations and published at the snapshot's generation.
As a fixture convenience, a top-level entry that declares no Verbs is assumed to advertise the verbs a followable type needs — a snapshot opts a resource in by setting Allowed, and spelling out get/list/watch on every fixture would be noise. Set Verbs explicitly to model a verb-poor resource.
func (*Registry) All ¶
func (r *Registry) All() []TypeRecord
All returns every known record — followable, retained, and refused — for inventory and "why not" views.
func (*Registry) ByGVK ¶
func (r *Registry) ByGVK(gvk schema.GroupVersionKind) (TypeRecord, bool)
ByGVK returns the record for a kind. The bool reports whether the kind is known to the registry at all; callers gate behaviour on record.Followable(). When a kind is served by more than one resource every such record is refused with gvk-not-unique, and the deterministic first (by GVR) is returned.
func (*Registry) ByGVR ¶
func (r *Registry) ByGVR(gvr schema.GroupVersionResource) (TypeRecord, bool)
ByGVR returns the record for a resource. The bool reports whether the resource is known to the registry at all.
func (*Registry) ByGroupResource ¶
func (r *Registry) ByGroupResource(group, resource string) []TypeRecord
ByGroupResource returns the records — one per served version, sorted by GVR — for a version-less (group, resource) pair, the shape per-type audit-stream keys carry. Existing TypeRecord fields answer everything else a caller needs: the version is Identity.GVR.Version, plus Preferred and Followable(). Records held under the removal grace are included (last-known versions), which is exactly the wobble-friendly answer a stream-key resolution wants during a discovery blink. Deliberately the WHOLE version surface: no version parameters thread through any other signature.
func (*Registry) Followable ¶
func (r *Registry) Followable() []TypeRecord
Followable returns every live record (verdict followable or retained), sorted by identity. It is the inventory the informer set and snapshot scope derive from.
func (*Registry) Generation ¶
Generation reports the catalog generation the current records were resolved at.
func (*Registry) Revision ¶
Revision reports the registry's change-of-decision counter. It bumps whenever the followable membership changes or the scan generation moves, so a consumer that caches a projection of the registry can gate its rebuild on this value and still react to a retention-grace drop that happens without any discovery change.
func (*Registry) Subscribe ¶
Subscribe registers an observer for every lifecycle event from subsequent Updates. Register before the first Update to observe cold-start activations. Observers are invoked outside the registry's read/write lock (so they may read the registry) but under the updater's serialization, so a slow observer stalls the updater — keep them non-blocking.
There is no production observer today, so the events computed on every Update are currently dispatched to nobody. That is deliberate rather than abandoned: this is the producer of the only signal that can tell a type genuinely withdrawn (a settled TypeRemoved, past RemovalGrace) from a discovery wobble, and mistaking the second for the first is what deletes a user's manifests. Its consumer is the `stop` classification in docs/design/target-watch-plan.md, "What a cell leaving means". Subscribing and classifying is the first half of that plan's third change and needs nothing decided first: a settled removal drops its cell from the plan, and the Git-side sweep converges the mirror under the target's existing spec.prune.mode. Only removal on INTENT waits on an open decision. Do not delete this as dead: rebuilding the settle window and the grace correctly is the expensive half, and it is already written and tested here. The Materializer, which consumed these events before the watch-first rewrite, is gone.
func (*Registry) Update ¶
func (r *Registry) Update(observations []Observation, generation uint64)
Update replaces the observation set for a new catalog generation and applies the live-set grace. Every observation becomes a record at this generation; a previously-live type missing from the set is re-judged as retained (within the grace) or dropped (once the grace elapses). The first Update marks the registry ready.
func (*Registry) UpdateFromScan ¶
UpdateFromScan publishes one normalized discovery scan, applying the unified "additions fast, removals slow" policy in this one place:
- entries in the scan become fresh trusted records (additions fast);
- a previously-known record whose group/version FAILED keeps its last-known facts marked untrusted — VerdictRetained for as long as the error persists (retain-on-error, moved here from the catalog);
- a previously-known record whose group/version was scanned (or which is gone from a complete scan) and is missing goes absent and rides the EXISTING RemovalGrace — Retained for the grace, then dropped (removals slow, with no instant prune anywhere);
- on an incomplete scan, an unscanned, non-failed group/version's records are carried unchanged: an incomplete scan judges nothing it did not see.
A record already inside its removal grace is never resurrected by a carry-forward; its absence clock keeps running until the type is freshly observed.
type Requirement ¶
type Requirement string
Requirement is one named check in the followability funnel.
const ( // RequirementServed — discovery serves this as a top-level resource. RequirementServed Requirement = "served" // RequirementTrusted — the backing group/version came from trusted discovery. RequirementTrusted Requirement = "trusted" // RequirementStable — the type is not mid-disappearance, or is inside the grace. RequirementStable Requirement = "stable" // RequirementIdentity — GVK <-> GVR is 1:1 in both directions. RequirementIdentity Requirement = "identity" // RequirementScope — the type is known namespaced or cluster-scoped. RequirementScope Requirement = "scope" // RequirementVerbs — discovery advertises get, list, watch, patch. RequirementVerbs Requirement = "verbs" // RequirementOrigin — classified builtin, crd, or aggregated with evidence. RequirementOrigin Requirement = "origin" // RequirementPolicy — product policy permits mirroring this type. RequirementPolicy Requirement = "policy" // RequirementSensitivity — not sensitive, or sensitivity is supported. RequirementSensitivity Requirement = "sensitivity" // RequirementScale — scale is unused, or its parent replica path is known. RequirementScale Requirement = "scale" )
type Result ¶
type Result string
Result is one requirement's outcome.
const ( // ResultPass — the requirement is satisfied. ResultPass Result = "pass" // ResultFail — the requirement is not satisfied; Reason names the single cause. ResultFail Result = "fail" // ResultSkip — the requirement does not apply (e.g. scale when scale is unused). ResultSkip Result = "skip" // ResultUnknown — the requirement could not be assessed. ResultUnknown Result = "unknown" )
type ScaleBinding ¶
type ScaleBinding struct {
Enabled bool
Source string // discovery | crd | builtin-registry | aggregated | unknown
ResponseGVK schema.GroupVersionKind
SpecReplicasPath string
StatusReplicasPath string
SelectorPath string
SelectorKind string // serialized-string | label-selector | unknown
// Usable is true only when a /scale audit event can be mapped back to a durable
// parent field. False feeds the scale requirement's scale-path-unresolved reason.
Usable bool
}
ScaleBinding is the only subresource fact the writer needs: where a /scale mutation lands on the parent's desired state. SpecReplicasPath drives the scale write path; the selector facts are for reporting. See docs/spec/type-followability.md.
func BuiltinScale ¶
func BuiltinScale(group, resource string) (ScaleBinding, bool)
BuiltinScale returns the /scale binding for a currently-served built-in scalable resource identified by API group and plural resource. ok is false for any other resource — a CRD, an aggregated API, or a non-scalable built-in — in which case the scale event must be resolved elsewhere or dropped, never defaulted to .spec.replicas. It is the single source of built-in scale facts, shared by the cluster registry (origin/scale enrichment) and the audit consumer (scale write).
type Scan ¶
type Scan struct {
// Entries are the resources this scan served, policy-annotated. All are trusted
// facts: a failed group/version contributes no entries (the registry carries its
// last-known records forward instead).
Entries []Entry
// ScannedGroupVersions are the group/versions this scan returned a (possibly
// empty) resource list for. A previously-known record of a scanned group/version
// that is missing from Entries is meaningfully absent — the removal grace judges
// it — even on an otherwise incomplete scan.
ScannedGroupVersions []schema.GroupVersion
// FailedGroupVersions are the group/versions discovery reported as failed
// (IsGroupDiscoveryFailedError). Their previously-known records are retained with
// last-known facts, marked untrusted, for as long as the failure persists.
FailedGroupVersions []schema.GroupVersion
// Complete reports a scan with no discovery error: only then is a wholly
// unscanned group/version's disappearance meaningful (and even then it rides the
// removal grace, never an instant prune).
Complete bool
// Generation is the catalog's scan generation — bumped by the caller only when
// the normalized facts changed, so registry revisions do not churn on steady
// rescans.
Generation uint64
}
Scan is one normalized discovery result — the per-scan facts a catalog scan produces, with no judgement attached. The catalog stays a thin normalizer; ALL cross-scan judgement ("additions fast, removals slow": retain-on-error, the removal grace for omissions) is applied by Registry.UpdateFromScan. See docs/spec/typeset-owns-discovery-grace.md.
type Scope ¶
type Scope string
Scope is whether a type is namespaced, cluster-scoped, or not yet known. Unknown feeds the scope requirement's scope-unknown reason; discovery normally resolves it, so Unknown is reserved for synthetic or un-enriched observations.
const ( // ScopeNamespaced is a namespaced resource (lives inside a namespace). ScopeNamespaced Scope = "Namespaced" // ScopeCluster is a cluster-scoped resource. ScopeCluster Scope = "ClusterScoped" // ScopeUnknown is a type whose scope discovery has not established. ScopeUnknown Scope = "Unknown" )
type Snapshot ¶
type Snapshot struct {
// Entries are the served resources the snapshot declares. Allowed defaults to
// false on a zero Entry, so fixtures opt resources in explicitly.
Entries []Entry
// DegradedGroupVersions mark group/versions whose discovery is modeled as failed;
// their entries are observed as untrusted (retained rather than freshly followable).
DegradedGroupVersions []schema.GroupVersion
// NotReady models a scan with no trusted data yet — the structure-only mode — so
// the resulting Lookup is never ready and judges nothing.
NotReady bool
// Generation is the reported scan generation.
Generation uint64
}
Snapshot is a serialized, scan-shaped fixture for a non-live Lookup. It is an explicit test/review input, not live discovery: it can model old clusters, partial catalogs, policy exclusions, and ambiguity on purpose, but must not be mistaken for proof about a running cluster.
type StatusFact ¶
type StatusFact struct {
Enabled bool
}
StatusFact records whether a type exposes a /status subresource. It is reporting only — GitOps Reverser never writes /status — so it carries no write path.
type Subresources ¶
type Subresources struct {
Status StatusFact
Scale ScaleBinding
}
Subresources are folded into the parent record, never followed as their own types.
type TypeRecord ¶
type TypeRecord struct {
Identity Identity
Origin Origin
Preferred bool
Verbs []string
Subresources Subresources
Sensitive bool
Followability Followability
Generation uint64
}
TypeRecord is the unit everything passes around. It answers "can I act on this?" and "why not?" in one object, so the safe path and the diagnostic path are the same call.
func (TypeRecord) Followable ¶
func (r TypeRecord) Followable() bool
Followable is the safe-path helper. Most callers never inspect Verdict directly: a followable or retained type is live, everything else is not.
type Verdict ¶
type Verdict string
Verdict is the top-level answer for one type. Every other surface (health level, status condition, "why ignored?" diagnostic) is a rendering of this.
const ( // VerdictFollowable means every required check passed; the type is in the live set. VerdictFollowable Verdict = "followable" // VerdictRetained means a transient check (served/trusted) is failing now but the // removal grace has not elapsed; the type is still treated as live. VerdictRetained Verdict = "retained" // VerdictRefused means a permanent check failed; the type will not be followed. VerdictRefused Verdict = "refused" // VerdictUnknown means the registry could not assess the type (catalog unavailable). VerdictUnknown Verdict = "unknown" )