typeset

package
v0.48.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

func (Check) Failed

func (c Check) Failed() bool

Failed reports whether the check is a hard fail.

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 — no trusted catalog data exists yet.
	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

func NewSnapshotRegistry(snap Snapshot) *Registry

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

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

func (r *Registry) Generation() uint64

Generation reports the catalog generation the current records were resolved at.

func (*Registry) Ready

func (r *Registry) Ready() bool

Ready reports whether the registry has accepted any observation set.

func (*Registry) Revision

func (r *Registry) Revision() uint64

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

func (r *Registry) Subscribe(obs Observer)

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

func (r *Registry) UpdateFromScan(scan Scan)

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"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL