Troubleshooting¶
When a Pacto shows a status you did not expect, this page tells you why and
what to change. Start with the status the operator wrote on the Pacto object:
it holds the verdict, the stage that produced it and one finding per problem.
kubectl describe pacto <name>
kubectl get pacto <name> -o yaml # the whole object
kubectl get pacto <name> -o yaml | yq '.status' # just the status, if you have yq
Runtime observations lists every finding code referenced below.
Reading the conditions¶
status.contractStatus says what the verdict is. The conditions say which
stage produced it, which is usually what you need to fix. There are three, and
each one's reason is a fixed identifier you can match on:
| Condition | Status | Reason | What happened |
|---|---|---|---|
ContractValid |
True |
Parsed |
The contract loaded and passed validation. |
ContractValid |
True |
ReferenceOnly |
Same, and the contract declares no spec.target, so nothing further is observed. |
ContractValid |
False |
Invalid |
The contract loaded and is structurally wrong. status.validation names the errors. |
ContractValid |
Unknown |
Unavailable |
The contract could not be obtained -- registry unreachable, auth rejected, tag not found. Validity is undetermined, so status.validation is deliberately left empty. |
RuntimeObserved |
True |
Found |
Runtime evidence was collected. |
RuntimeObserved |
False |
ObservationFailed |
A cluster query errored. The message carries the API error. |
ReadinessSatisfied |
True |
Satisfied |
score >= minScore. |
ReadinessSatisfied |
False |
BelowMinScore |
The gate is not met. The message breaks the score down by claim status. |
ReadinessSatisfied |
False |
Expired |
The assessment is past its expires: date, which scores it 0. |
Two absences are meaningful:
RuntimeObservedis missing entirely when the reconciliation never got as far as observing — a reference-only contract, or one that failed atContractValid. That is not a failed observation.ReadinessSatisfiedis missing entirely when the contract declares noreadiness:block.
Conditions are sticky: each keeps its lastTransitionTime while its status is
unchanged, and carries the observedGeneration it was set from. A condition
whose observedGeneration is behind metadata.generation was not re-evaluated
on the latest spec.
Reading the events¶
Conditions say what the state is; the Events: block that closes
kubectl describe pacto <name> says what changed, including things no field
records — a tag force-pushed underneath you, or a readiness gate flipping.
kubectl describe pacto <name> # events are the last block
kubectl get events --field-selector involvedObject.name=<name> \
--sort-by=.lastTimestamp
The operator emits exactly eight, and no others:
| Reason | Type | Emitted when | What it tells you |
|---|---|---|---|
ContractInvalid |
Warning |
The contract was obtained and judged invalid | Carries the same message as the ContractValid / Invalid condition. Fix the contract. |
ContractUnavailable |
Warning |
The contract could not be obtained at all | Registry unreachable, auth rejected, tag missing. See Contract not resolving. |
ValidationFailed |
Warning |
A contract that did load ends the reconcile as anything but Compliant or Reference, and the status differs from the one the previous reconcile persisted |
Carries the counts -- ContractStatus: NonCompliant, 2 errors, 1 warnings. status.findings names each one. |
ContractRecovered |
Normal |
The contract status returned to Compliant or Reference from any other value |
The other half of the three warnings above. |
RevisionCreated |
Normal |
A PactoRevision was created for a newly resolved contract |
Created revision <name> for contract v<version>. Expected on the first resolve and on every version change; not a problem. |
TagOverwritten |
Warning |
A tag that already resolved now points at a different digest | Someone force-pushed the tag. See Choosing a reference form. |
ReadinessGateUnmet |
Warning |
The readiness gate went from met to unmet | The message breaks the score down by claim status. |
ReadinessRecovered |
Normal |
The readiness gate went from unmet to met | The other half of the pair above. |
Three things about them are easy to misread:
ValidationFailedoverstates theUnknowncase. Nothing failed validation there — an assertion could not be evaluated — and the event's own counts say so:ContractStatus: Unknown, 0 errors, 0 warnings. Read the counts, not the reason.- Every event except
RevisionCreatedandTagOverwrittenis transition-gated. A contract that stays broken emits one event, not one per reconcile, so aCountabove 1 means the status flapped, or the same reason recurred after a recovery. Two exceptions still repeat without either:TagOverwritten(see Choosing a reference form) and a revision-mirror failure, which is recorded once per reconcile pass. - Events expire, after the API server's
--event-ttlof one hour by default. An absent event is not evidence that it never fired: conditions andstatusare the durable record.
Chasing the evidence behind a finding¶
Every finding carries evidenceRefs, and it is worth knowing what they are
before you go looking for something that is not there:
evidenceRefs:
- source: k8s
observedAt: "2026-08-22T21:31:04Z"
collector: k8s-observer
detectedAt: "2026-08-22T21:31:04Z"
That is the whole record — which collector saw it and when. collector is
k8s-observer for the Kubernetes API alone, k8s-observer+ebpf when node
agent evidence decided or qualified the result and
ebpf when only the kernel saw it. A finding with no matched observation, such
as EVIDENCE_MISSING, carries source and observedAt only. There is no body
to fetch: the operator's observations are recomputed every reconcile and never
stored, so the reference dates the reading rather than retrieving it. The node
agent's raw socket view is the one exception you can read back, in the
NodeRuntimeObservation object named after the pod's node. What it
is good for is age: an observedAt that stopped advancing while
metadata.generation moved on is a leftover from a reconcile that no longer
runs.
To re-observe, nudge the resource. Any update enqueues a reconcile, and the annotation key is arbitrary:
Observations that must outlive the reconcile that made them — an audit trail, a signed record — are the Evidence Server, which writes to your registry. Cluster status is a live reading, not a log.
Status is Unknown¶
A required assertion could not be evaluated — not a violation. The finding code says why:
EVIDENCE_MISSING— nothing was collected. The target may not exist yet, orspec.target.serviceNamedoes not match a real Service.OBSERVATION_UNSUPPORTED— the dimension cannot be observed here: anExternalNamedependency, or a metrics capability while--enable-metrics-observationis off.COLLECTION_FAILED— the cluster query errored, or the node agent could not load its kernel program. Check theRuntimeObservedcondition and the operator logs.EVIDENCE_STALE— the node agent on the pod's node stopped reporting. See Node agent evidence is missing.EVIDENCE_INSUFFICIENT— some evidence arrived but not enough to decide, for example the node agent saw only part of a pod.
A contract that could not be obtained transiently also reads Unknown rather
than Invalid.
Status is Invalid¶
Structural validation failed or the artifact could not be parsed (fail-closed).
status.validation carries the SCHEMA_VIOLATION or parse error; reproduce it
locally:
Status is NonCompliant¶
At least one confirmed violation. The two configuration codes do not arrive at
the same speed. A mismatch (CONFIGURATION_MISMATCH) is NonCompliant on
the first reconcile that observes it. An absence (CONFIGURATION_ABSENT)
reads Unknown until the negative streak spans the
stabilization window.
Status is Reference¶
The contract declares no spec.target: parsed and validated, never observed.
Add a spec.target to enable runtime observation.
Contract not resolving¶
- Confirm the OCI reference is reachable and, for private registries, that a pull
secret is set via
spec.contractRef.pullSecretRef. - For an unversioned reference the operator tracks the highest semver tag; a registry with no valid semver tags resolves nothing.
- Inspect the created
PactoRevisionresources:kubectl get pactorevisions.
Metrics or health always Unknown¶
The metrics dimension returns Unsupported unless --enable-metrics-observation
is set, and active health probing requires --enable-probing. The chart sets
them through controller.enableMetricsObservation and
controller.enableProbing — see
Opt-in features.
With probing off, health falls back to the workload, which needs an httpGet
readiness probe on the container behind the health capability's port plus a
Ready endpoint for it. When the capability declares a path, the probe's path
must equal it. A liveness-only pod, an exec or tcpSocket probe or
no probe leaves nothing to read. Passive observation can confirm health but
never contradict it, so Unknown is the honest answer here, not a broken one.
Operator RBAC errors¶
If the logs show forbidden errors reading Services, workloads or EndpointSlices,
confirm the operator's ClusterRole is installed. See RBAC. The chart
adds read access to ServiceMonitor and PodMonitor only when
controller.enableMetricsObservation is on. If you set the controller flag by
hand instead, grant that access yourself, as
Opt-in features shows.
Node agent evidence is missing¶
The node agent is the optional DaemonSet (one pod per node) behind
runtime conformance. If findings never carry
collector: k8s-observer+ebpf, check these in order:
kubectl -n pacto-node-agent get daemonset pacto-node-agent
kubectl -n pacto-node-agent get noderuntimeobservations,leases
kubectl -n pacto-operator-system logs deploy/pacto-operator | grep node-agent
- No DaemonSet.
nodeAgent.enabledis off, or the agent runs in anothernodeAgent.namespace. - DaemonSet with no pods. Something rejects them in the agent namespace.
The operator sets its Pod Security labels to
privilegedat every sync; readkubectl -n pacto-node-agent describe daemonset pacto-node-agentfor the admission message, for example from a policy engine of your own. - Pods running, no
NodeRuntimeObservationor Lease for a node. That node's findings readEVIDENCE_STALE. Read the agent pod's log on that node. A write refused withmay only write the objects named after its nodeorcarries no node namecomes from the chart's admission policy: the agent's token does not name its node, which needs Kubernetes 1.30 or later. - A Lease older than its node's observation. The Lease is the heartbeat;
the operator reads whichever of its
renewTimeand the observation'slastPublishedis newer. - Collection
Failed. The kernel could not load the program, often because it lacks BTF (the kernel type information at/sys/kernel/btf/vmlinux). Turn it on lists what each node needs.