Documentation
¶
Overview ¶
Package ops is the operations layer every human-facing surface sits on: the CLI, the API server, the MCP server and the UI.
It exists so that those four do not each answer the same questions separately. The answers that matter are *rules* — what counts as eligible, who may approve, why a Gate is stuck — and three implementations of a rule is three subtly different products. Adapters may format; they may not decide.
Thin over the Kubernetes API, and deliberately not a second data model: reads return the API types themselves. The only shapes defined here are ones that carry genuinely derived information — an explanation is not stored anywhere, which is precisely why it belongs in one place.
Index ¶
- func IsNotFound(err error) bool
- func IsRefused(err error) bool
- type AuditEntry
- type AuditKind
- type Blocker
- type BlockerKind
- type Evidence
- type Explanation
- type NotFoundError
- type Ops
- func (o *Ops) Abort(ctx context.Context, namespace, passageName, actor string) error
- func (o *Ops) Approve(ctx context.Context, namespace, bundleName, gateName, actor string) error
- func (o *Ops) Audit(ctx context.Context, namespace string) ([]AuditEntry, error)
- func (o *Ops) Beacon(ctx context.Context, namespace, name string) (*v1alpha1.Beacon, error)
- func (o *Ops) Beacons(ctx context.Context, namespace string) ([]v1alpha1.Beacon, error)
- func (o *Ops) Bundle(ctx context.Context, namespace, name string) (*v1alpha1.Bundle, error)
- func (o *Ops) Bundles(ctx context.Context, namespace string) ([]v1alpha1.Bundle, error)
- func (o *Ops) Evidence(ctx context.Context, namespace, bundleName string) (*Evidence, error)
- func (o *Ops) Explain(ctx context.Context, namespace, name string) (*Explanation, error)
- func (o *Ops) Gate(ctx context.Context, namespace, name string) (*v1alpha1.Gate, error)
- func (o *Ops) Gates(ctx context.Context, namespace string) ([]v1alpha1.Gate, error)
- func (o *Ops) Namespaces(ctx context.Context) ([]string, error)
- func (o *Ops) Passage(ctx context.Context, namespace, name string) (*v1alpha1.Passage, error)
- func (o *Ops) Passages(ctx context.Context, namespace, gate, bundle string) ([]v1alpha1.Passage, error)
- func (o *Ops) Poll(ctx context.Context, namespace, beaconName string) (string, error)
- func (o *Ops) Promote(ctx context.Context, namespace, gateName, bundleName, actor string) (*v1alpha1.Passage, error)
- type RefusedError
- type State
- type Waiting
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsNotFound ¶
IsNotFound reports whether err is a NotFoundError.
Types ¶
type AuditEntry ¶ added in v0.3.0
type AuditEntry struct {
At metav1.Time `json:"at"`
Kind AuditKind `json:"kind"`
Gate string `json:"gate"`
Bundle string `json:"bundle,omitempty"`
// Digest is what actually shipped. The Bundle name is a label; this is the
// content address, and it is the only field that answers "which bits".
Digest string `json:"digest,omitempty"`
// Actor is who caused it. Empty means nobody did — an automatic Gate acting
// on its own, which is a meaningful answer rather than a missing one.
Actor string `json:"actor,omitempty"`
Passage string `json:"passage,omitempty"`
// Detail is why, for a refusal, in the words of whatever refused.
Detail string `json:"detail,omitempty"`
// Verified records whether a crossing's verification confirmed it worked,
// which is a different question from whether it completed.
Verified *bool `json:"verified,omitempty"`
// Evidence links the entry to the compliance record: the Fides trail, the
// verdict, and any blockers. This is what turns "it was promoted" into
// "it was promoted, and here is what said it could be".
Evidence *v1alpha1.EvidenceRef `json:"evidence,omitempty"`
}
AuditEntry is one thing that happened, in terms an auditor asks about.
type AuditKind ¶ added in v0.3.0
type AuditKind string
AuditKind is what happened.
const ( // AuditCrossed is a Bundle that reached a Gate. AuditCrossed AuditKind = "crossed" // AuditRefused is a crossing that was attempted and did not complete. // // The most valuable entry on the page. A trail of everything that shipped // is a deployment log; what makes it an audit trail is that it also holds // what was stopped, by what, and on whose evidence. AuditRefused AuditKind = "refused" // AuditRunning is a crossing in progress. AuditRunning AuditKind = "running" // AuditApproved is a human approving a Bundle for a Gate. Separate from a // crossing because approving and crossing are separate acts by separate // people — an approval a promoter can grant themselves is not an approval. AuditApproved AuditKind = "approved" )
type Blocker ¶
type Blocker struct {
// Kind is a stable code, so a caller can branch without parsing prose.
Kind BlockerKind `json:"kind"`
// Detail is the human-readable specifics.
Detail string `json:"detail"`
// Fix is what would unblock it, when there is a single obvious answer.
Fix string `json:"fix,omitempty"`
}
Blocker is one reason nothing is crossing.
type BlockerKind ¶
type BlockerKind string
BlockerKind enumerates why a Gate is not crossing. Stable strings: a UI chooses an icon from these and an LLM reasons over them.
const ( BlockerSuspended BlockerKind = "Suspended" BlockerInvalidSteps BlockerKind = "InvalidSteps" BlockerNoPassage BlockerKind = "NoPassageTemplate" BlockerNoBundles BlockerKind = "NoBundles" BlockerNotApproved BlockerKind = "AwaitingApproval" BlockerUpstream BlockerKind = "UpstreamNotCleared" BlockerWindowClosed BlockerKind = "WindowClosed" BlockerPassageFailed BlockerKind = "PassageFailed" BlockerStepWaiting BlockerKind = "StepWaiting" BlockerChangeHeld BlockerKind = "ChangeHeld" BlockerUnhealthy BlockerKind = "Unhealthy" BlockerManual BlockerKind = "AwaitingRequest" )
type Evidence ¶
type Evidence struct {
Bundle string `json:"bundle"`
Namespace string `json:"namespace"`
// Digest is the artifact all of this is about. Named explicitly because the
// evidence belongs to the image, not to the Bundle that happens to pin it.
Digest string `json:"digest,omitempty"`
// Trail is the Fides trail this evidence lives on. Reported as the bare
// identifier rather than a link: the portal is a single-page export with
// no per-trail route to deep-link to, and a link that 404s is worse than
// an id an auditor can paste.
Trail string `json:"trail,omitempty"`
// Gate is which Gate's Fides configuration was used to look this up, so a
// reader can tell which environment's view they are seeing.
Gate string `json:"gate,omitempty"`
// Verdict is the change gate's answer, with its controls, attestation
// counts, approvals and segregation-of-duties finding.
Verdict *fides.ChangeVerdict `json:"verdict,omitempty"`
// ApprovedIn lists the approvals Hecate itself recorded, which is not the
// same list as Fides' — a Gate that does not use Fides still has approvers,
// and they belong in the answer to "who allowed it".
ApprovedIn []v1alpha1.BundleApproval `json:"approvedIn,omitempty"`
// A panel that renders empty is indistinguishable from a clean bill of
// health, and those are opposite answers.
Unavailable string `json:"unavailable,omitempty"`
}
Evidence is everything Fides holds about one Bundle's artifact, assembled to answer a single question: why was this allowed into production, and who allowed it?
Answering it today means opening Fides, finding the trail, and reading four pages. This is that answer in one object, so a CLI can print it and the UI can show it without the reader leaving the page.
type Explanation ¶
type Explanation struct {
Gate string `json:"gate"`
Namespace string `json:"namespace"`
State State `json:"state"`
// Summary is one line, suitable on its own.
Summary string `json:"summary"`
// Blockers are the specific things standing in the way, most actionable
// first. Empty when nothing is wrong.
Blockers []Blocker `json:"blockers,omitempty"`
// Current is the Bundle in the environment now, if any.
Current string `json:"current,omitempty"`
// Eligible names Bundles that could cross right now.
Eligible []string `json:"eligible,omitempty"`
// Waiting names Bundles that cannot, with the reason for each.
Waiting []Waiting `json:"waiting,omitempty"`
// Health is the Gate's own assessment of what it watches.
Health v1alpha1.Health `json:"health,omitempty"`
// Evidence is the change gate's verdict for the crossing in progress, when
// there is one. Carried whole so a caller can show the risk score next to
// the reasons rather than parsing them back out of a sentence.
Evidence *v1alpha1.EvidenceRef `json:"evidence,omitempty"`
}
Explanation is why a Gate is where it is.
The question "why is nothing crossing?" is currently answered by reading four resources by hand and knowing which fields matter. This answers it once, in structure a CLI can print, an API can serialise and a model can reason over — which is why the reasons are a list of typed causes rather than a sentence.
type NotFoundError ¶
NotFoundError is a named resource that is not there.
Distinguished from any other failure because every surface needs to answer differently: the API server owes a 404, the CLI a exit code, and the MCP server a message the model can act on rather than retry.
func (*NotFoundError) Error ¶
func (e *NotFoundError) Error() string
type Ops ¶
type Ops struct {
Client client.Client
// Now is the clock, injectable for tests and for judging promotion windows.
Now func() metav1.Time
// FidesServer is the default Fides server, used when a Gate names none of
// its own. Approvals are recorded there so segregation of duties can be
// evaluated over real identities.
FidesServer string
// DialFides is injectable so tests can point at a fake Fides. Nil is the
// real client.
DialFides func(fides.Config) (*fides.Client, error)
}
Ops answers questions and performs actions against a cluster.
func New ¶
New returns an Ops over the given client.
The Fides server defaults from FIDES_SERVER_URL — the same variable the controller's --fides-server reads — so the CLI, the API server and the MCP server all find it without three flags that have to agree. A Gate's own evidence.serverURL still wins.
func (*Ops) Abort ¶
Abort asks a running Passage to stop.
It sets spec.abort rather than deleting: the Passage is the record of what happened, and deleting it would erase the evidence that a crossing was started and stopped (D18). The controller marks the remaining steps aborted.
func (*Ops) Approve ¶
Approve records that a human has approved a Bundle for a Gate.
Approval is per Gate, not per Bundle: approving something for staging must not approve it for production, which is the whole point of asking.
func (*Ops) Audit ¶ added in v0.3.0
Audit reconstructs what has happened in a namespace, newest first.
Built from two sources that outlive each other differently, which is the reason it is not simply a list of Passages.
A Passage is the detailed record — actor, per-step outcome, the evidence verdict, why it stopped — and it is subject to retention: a Gate keeps a bounded number, so the detail ages out. `Gate.status.history` is the durable one, capped but long-lived, and it survives the Passage it names.
So history is the spine and Passages enrich it, and a Passage with no history entry still appears — that is exactly the refused crossing, which never entered the Gate and therefore was never recorded there.
func (*Ops) Bundles ¶
Bundles lists a namespace's Bundles, newest first — the order an operator expects, because the question is nearly always about the recent ones.
func (*Ops) Evidence ¶
Evidence assembles the compliance record for a Bundle.
**It never fails for want of evidence.** No Gate configured for Fides, no digest pinned, no trail on the artifact — each is a fact about this deployment rather than an error, and each is reported in `unavailable` so the caller can say which. Only a Fides that is configured and then does not answer is an error, because that one is worth retrying.
func (*Ops) Explain ¶
Explain answers "why is this Gate not crossing anything?".
It composes the rules rather than restating them: eligibility comes from pkg/gate's own judgement, and the window from its own check. A second implementation of either would be a second answer to the same question, which is the failure this package exists to prevent.
func (*Ops) Gates ¶
Gates lists a namespace's Gates, by name.
Ordered rather than left in list order: a surface that prints them should print them the same way twice, and a diffable list is worth more than a fast one at this size.
func (*Ops) Namespaces ¶ added in v0.2.0
Namespaces lists the namespaces that hold something Hecate manages.
Not "every namespace in the cluster". A picker offering three hundred namespaces of which two are relevant is a worse answer than a text box, and listing all of them needs a cluster-wide read on core Namespaces that a Gate operator has no other reason to hold.
Gates and Beacons are enough to find them all: a Bundle is created in its Beacon's namespace and a Passage in its Gate's, so a namespace holding either of those holds one of these too.
The caller is responsible for removing namespaces the user may not see. This runs with the server's own credentials, so filtering is not optional — it is done in the API layer, where the subject is known (pkg/api).
func (*Ops) Passages ¶
func (o *Ops) Passages(ctx context.Context, namespace, gate, bundle string) ([]v1alpha1.Passage, error)
Passages lists a namespace's Passages, newest first. Filters are optional: an empty gate or bundle means "any".
func (*Ops) Poll ¶
Poll asks a Beacon to look at its sources now, rather than at its next interval.
**This is the whole of the webhook receiver (#102).** A Beacon already polls immediately when Flux's `reconcile.fluxcd.io/requestedAt` annotation changes, and that path is proven end to end. What a git host could not do was set a Kubernetes annotation — so the missing piece was an HTTP door onto the mechanism, not a mechanism.
It needs no shared secret and no HMAC verification, which is the posture Flux v2.9 moved to with OIDC-secured Receivers. The API server authenticates this call the same way it authenticates every other one, by asking Kubernetes to review the bearer token; a cluster configured to trust a CI provider's OIDC issuer therefore accepts that provider's workload token here with nothing added. A secret nobody stores is a secret nobody leaks.
The token returned is echoed back in `status.lastHandledReconcileAt`, so a caller can tell its own request apart from someone else's.
func (*Ops) Promote ¶
func (o *Ops) Promote(ctx context.Context, namespace, gateName, bundleName, actor string) (*v1alpha1.Passage, error)
Promote asks a Gate to cross a Bundle, and returns the Passage it opened.
The eligibility rules are the Gate controller's, not a second set: a crossing requested by hand is judged exactly as an automatic one would be. Skipping the check for manual requests is how "promote to production" becomes a way to bypass the pipeline.
type RefusedError ¶
RefusedError is an action the rules do not allow.
Separate from a failure to perform it: "this Bundle has not cleared staging" is an answer, not an error, and every surface presents the two differently.
func (*RefusedError) Error ¶
func (e *RefusedError) Error() string
type State ¶
type State string
State is the one-word answer to "what is this Gate doing?".
const ( // StateCrossing means a Passage is running now. StateCrossing State = "Crossing" // StateReady means a Bundle could cross and is waiting to be asked. StateReady State = "Ready" // StateBlocked means something must change before anything can cross. StateBlocked State = "Blocked" // StateIdle means there is nothing to do: nothing new has appeared. StateIdle State = "Idle" // StateFailed means the last crossing failed and has not been retried. StateFailed State = "Failed" )
type Waiting ¶
type Waiting struct {
Bundle string `json:"bundle"`
Reason string `json:"reason"`
// Kind is the same reason as a stable code, matching BlockerKind's values
// where they overlap. Without it the approval queue would have to decide
// what is waiting on a human by matching the prose, and rewording a message
// would break a caller.
Kind gate.Code `json:"kind,omitempty"`
}
Waiting is one Bundle that cannot cross, and why.