Skip to content

TrianaLab/pacto-operator

CI codecov Go Report Card GitHub Release License: MIT Artifact Hub

Pacto Operator

Kubernetes operator that checks whether running workloads match their declared Pacto service contracts.

The operator watches Pacto custom resources, reads the referenced contract, observes the live workload, and reports whether they align. It is read-only and non-intrusive — it never modifies your workloads.


Where it fits

Pacto is a service contract system. Three components cover the full lifecycle:

Component Role
CLI Author, validate, diff, and publish contracts to OCI registries
Operator (this repo) Continuously check runtime alignment between contracts and live workloads
Dashboard Visualize the service graph, dependency tree, and compliance status

The CLI is the authoring tool. The operator is the runtime feedback loop. The dashboard makes the results visible.


Why

Teams declare intent in a contract — workload type, upgrade strategy, images, probes, storage — and deploy separately through Helm or Kustomize. Nothing connects those two sides at runtime. Contracts drift from reality silently.

The operator closes this gap. It reads the contract, observes the live workload, and reports whether they match. Continuously, without modifying anything.


Architecture

flowchart LR
    subgraph Cluster
        API[(K8s API)]
        Workloads[Workloads]
    end

    subgraph Controller
        Loader
        Observer
        Validator
    end

    CR[Pacto CR] --> Loader
    Observer -- reads --> API
    API -.- Workloads
    Loader --> Validator
    Observer --> Validator
    Validator --> Status[Status + Conditions]
    Validator --> Metrics[Prometheus Metrics]
Loading

Each reconciliation follows a fixed pipeline:

  1. Loader resolves the contract from an OCI registry (auto-selecting the highest semver tag) or parses inline YAML.
  2. Observer reads runtime state from the Kubernetes API — workload kind, strategy, images, probes, volumes, termination grace period.
  3. Validator is a pure function: (contract, snapshot, hasService) → result. The hasService flag tells the validator whether a runtime target was bound, so it can distinguish a missing Service from reference-only mode. No side effects.
  4. Controller coordinates the pipeline, creates PactoRevision snapshots for each resolved version, and updates the CR status with structured conditions, a contract compliance status, and metrics.

Runtime checks

The following checks run on each reconciliation. These are the current built-in checks — they cover the most common contract-to-runtime mismatches, not every possible validation.

Check Severity What it validates
WorkloadType error Deployment vs StatefulSet vs Job matches contract
StateModel error PVC/emptyDir presence matches contract state model
UpgradeStrategy warning RollingUpdate vs Recreate vs OrderedReady matches contract
GracefulShutdown warning terminationGracePeriodSeconds matches contract
Image warning Container image matches contract
HealthTiming warning Probe initialDelaySeconds matches contract

Error-severity failures set the contract status to NonCompliant. Warning-severity failures set it to Warning. When all checks pass, the status is Compliant.

These runtime checks are structural-only: they compare the declared contract (image, upgrade strategy, probe timing, storage, workload type, state model) against the observed workload spec — no traffic is sent. Separately, the operator probes the contract's declared health and metrics endpoints for reachability. Those probes are asynchronous network calls; their results are recorded in status.endpoints and as HealthEndpointValid/MetricsEndpointValid conditions. A failed endpoint probe surfaces as a Warning — it does not drive the contract to NonCompliant the way a missing Service or workload (a structural failure) does.

Note: ContractStatus reflects contract validation/compliance, not runtime health.


Readiness

The readiness section requires pactoVersion: "1.1" — this is structurally enforced by the contract JSON schema, so contracts that declare readiness under an older pactoVersion are rejected by the CLI before they ever reach the operator.

When a contract declares a readiness section, the operator computes a derived readiness assessment and writes it to status.readinessscore, minScore, passing, totalWeight, currentWeight, currentCount, expiredCount, and a per-check list (statusCurrent/Expired/Invalid, plus daysRemaining). It is computed from the declared weight/expires/minScore values and the current time; the evidence targets are never fetched or verified.

Readiness is a separate dimension from contract compliance — it never changes ContractStatus. The operator surfaces the gate (score >= minScore, with minScore defaulting to 100 when omitted) through one aggregate condition, ReadinessSatisfied:

Status Reason Meaning
True Satisfied the readiness score meets minScore
False Invalid the gate is unmet and at least one check has an unparseable expires date
False BelowMinScore the gate is unmet with all expiry dates parseable (e.g. checks expired)

The reason precedence when the gate is unmet is: Invalid whenever any check has an invalid expiry (InvalidCount > 0), otherwise BelowMinScore.

On gate transitions it emits events sparingly: a Warning/ReadinessGateUnmet when the gate first drops and a Normal/ReadinessRecovered when it is met again. Contracts without readiness get neither status.readiness nor the condition.

For the canonical score and gate semantics, see the readiness reference in the CLI docs.


CRDs

Pacto

A Pacto resource binds a contract source to an optional runtime target:

  • Contract source: OCI registry reference (spec.contractRef.oci) or inline YAML (spec.contractRef.inline).
    • Unversioned (ghcr.io/org/name): tracks the latest semver tag, re-resolved on every reconciliation.
    • Tagged (ghcr.io/org/name:1.2.3): pinned to that exact tag, no automatic updates.
    • Digest (ghcr.io/org/name@sha256:abc...): immutable, always resolves to that exact content.
    • The resolved mode is reported in status.resolutionPolicy (Latest, PinnedTag, or PinnedDigest).
  • Private registries: set spec.contractRef.pullSecretRef to the name of a Kubernetes Secret (in the same namespace) containing OCI credentials. See Private OCI Registries.
  • Target: a Kubernetes Service (spec.target.serviceName) and workload (spec.target.workloadRef). If the workload ref is omitted, it defaults to a Deployment with the same name as the service.
  • Reference mode: when no target is specified, the Pacto is reference-only — the contract is resolved and stored, but no runtime validation runs. ContractStatus is Reference.
  • Reconciliation frequency: spec.checkIntervalSeconds is the requeue delay the operator returns after each reconciliation (default: 300, minimum: 30) — it is not a background poll. The operator also reconciles immediately whenever the spec, status, or a referenced Secret changes, then requeues itself after this interval.

PactoRevision

A PactoRevision is an immutable snapshot of a resolved contract version. Created automatically when a new version is resolved. Owned by the parent Pacto resource and garbage collected on deletion. Name pattern: <pacto-name>-<version>-<hash>.


Quick Start

  1. Install the operator (see Installation).

  2. Create a Pacto resource that binds a contract to a workload:

    apiVersion: pacto.trianalab.io/v1alpha1
    kind: Pacto
    metadata:
      name: my-service
    spec:
      contractRef:
        oci: ghcr.io/your-org/contracts/my-service
      target:
        serviceName: my-service

    The operator resolves the highest semver tag from the OCI registry, creates a PactoRevision for that version, observes the my-service Deployment and Service, runs all checks, and sets the contract status.

  3. Check status:

    kubectl get pactos

    The STATUS column shows: Compliant (all checks pass), Warning (non-critical mismatches), NonCompliant (errors or missing resources), or Reference (no target).

  4. Inspect conditions for details on individual checks:

    kubectl describe pacto my-service

Breaking change: status.phase removed

The status.phase field has been removed. Use status.contractStatus instead.

Before (status.phase) After (status.contractStatus)
Healthy Compliant
Degraded Warning
Invalid NonCompliant
Reference Reference
Unknown Unknown

kubectl get pactos now shows a STATUS column (was PHASE).

ContractStatus reflects contract validation/compliance, not runtime health. Update any scripts, dashboards, alerts, or integrations that read .status.phase to use .status.contractStatus.


Installation

Helm (recommended)

helm install pacto-operator oci://ghcr.io/trianalab/charts/pacto-operator \
  --namespace pacto-operator-system --create-namespace

The dashboard is enabled by default. See the chart README for all configuration options including Service type, Ingress, and Gateway API HTTPRoute.

Kustomize

make install   # Install CRDs
make deploy    # Deploy the controller

Private OCI Registries

If your contracts are stored in a private OCI registry, create a Kubernetes Secret with credentials and reference it from the Pacto CR via spec.contractRef.pullSecretRef.

The Secret must be in the same namespace as the Pacto CR and contain one of:

  • token — a bearer/registry token (e.g. a GitHub PAT), or
  • username + password — basic auth credentials

These are mutually exclusive — if token is present it takes precedence.

Example using a GitHub token:

kubectl create secret generic ghcr-creds \
  --from-literal=username=x-access-token \
  --from-literal=password="$(gh auth token)" \
  -n my-namespace

Example Pacto CR:

apiVersion: pacto.trianalab.io/v1alpha1
kind: Pacto
metadata:
  name: my-service
  namespace: my-namespace
spec:
  contractRef:
    oci: ghcr.io/my-org/contracts/my-service
    pullSecretRef: ghcr-creds
  target:
    serviceName: my-service

The operator watches the referenced Secret — if credentials are rotated, the next reconciliation uses the updated values automatically.

If the Secret is missing or has invalid keys, the Pacto CR status is set to NonCompliant with a clear error message.

Note: spec.contractRef.pullSecretRef provides credentials for the operator to pull contracts. The separate dashboard.ociSecret Helm value provides credentials for the dashboard pod. These are independent configurations.


Dashboard

The operator optionally manages a Pacto Dashboard instance. The dashboard provides a visual service graph showing dependencies, contract versions, and compliance status across all Pacto resources in the cluster.

The operator handles the full dashboard lifecycle: Deployment, ClusterIP Service, ServiceAccount, and RBAC. The dashboard image is version-locked to the Pacto library bundled into the controller.

Dashboard CPU/memory requests and limits can be overridden via the chart's dashboard.resources values (which set the controller's --dashboard-cpu-request / --dashboard-cpu-limit / --dashboard-memory-request / --dashboard-memory-limit flags). These accept standard Kubernetes resource quantity strings — 100m, 256Mi, 1Gi, and so on. Every supplied value is parsed at operator startup, so an invalid quantity fails fast (the operator refuses to start) rather than panicking during the first reconciliation.

Network exposure is a chart-level concern. The Helm chart creates a separate configurable Service for external access, with optional Ingress and Gateway API HTTPRoute support. See the chart README for details.


Metrics

The controller exposes Prometheus metrics via OpenTelemetry:

Metric Type Labels Description
pacto_contract_status Gauge name, namespace, status Info-style gauge: 1 for the current status, 0 for all others. Status is one of: Compliant, Warning, NonCompliant, Reference, Unknown
pacto_contract_compliance_status Gauge service, namespace 1 = compliant, 0 = non-compliant
pacto_contract_validation_errors Gauge service, namespace Count of error-severity failures
pacto_contract_validation_warnings Gauge service, namespace Count of warning-severity mismatches
pacto_contract_validation_result Gauge service, namespace, check Per-check result (1=pass, 0=fail)

Enable a Prometheus ServiceMonitor via Helm:

metrics:
  serviceMonitor:
    enabled: true

Pre-built PrometheusRule alerting templates are available in config/prometheus/alerts.yaml. Apply them manually:

kubectl apply -f config/prometheus/alerts.yaml

What it does NOT do

  • Enforce or block deployments. The operator is read-only. It reports drift; it does not prevent it. Use admission webhooks or CI gates if you need enforcement.
  • Author or publish contracts. That is the CLI's job.
  • Modify workloads. It never patches, scales, restarts, or deletes your resources.
  • Deep protocol validation. It probes declared health and metrics endpoints for reachability but does not validate OpenAPI responses or run integration tests. It checks structural properties (image, strategy, probes, storage) declared in the contract.
  • Replace monitoring. It answers "does the workload match the contract?", not "is the workload healthy?". Use it alongside — not instead of — observability tools.

Artifact Verification

All published artifacts (controller image and Helm chart) are signed with Cosign using keyless OIDC signing via GitHub Actions.

Verify the controller image:

cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'github\.com/TrianaLab/pacto-operator' \
  ghcr.io/trianalab/pacto-operator/pacto-controller:<version>

Verify the Helm chart:

cosign verify \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'github\.com/TrianaLab/pacto-operator' \
  ghcr.io/trianalab/charts/pacto-operator:<version>

Development

Prerequisites

  • Go 1.26+
  • Docker
  • kubectl
  • Kind (for local Kubernetes and e2e tests)
  • make

Build and test

make build        # Build the controller binary
make test         # Run unit/integration tests (envtest)
make ci           # Run static checks + unit tests + chart validation (no cluster required)
make test-e2e     # Run e2e tests (requires Kind — creates and tears down a cluster)
make lint         # Run golangci-lint

make ci mirrors the CI pipeline's static, unit-test, and chart jobs. The e2e job requires a Kind cluster and runs separately via make test-e2e.

Local development

Local process (operator runs on your machine, connects to current kube context):

make run                    # Operator without dashboard
make run-with-dashboard     # Operator with dashboard enabled

Local Kubernetes (operator runs inside a local cluster as a container):

make deploy-local                  # Build, install CRDs, deploy (any kube context)
make deploy-local-with-dashboard   # Build, install CRDs, deploy with dashboard
make undeploy-local                # Remove from current kube context

These targets work with any local Kubernetes distribution (Docker Desktop, minikube, Kind, etc.). If you use Kind, run make kind-load first so the image is available inside the cluster, or use make deploy-kind which combines both steps.

See CONTRIBUTING.md for the full development guide.


Artifacts

Artifact Location
Controller image ghcr.io/trianalab/pacto-operator/pacto-controller
Helm chart oci://ghcr.io/trianalab/charts/pacto-operator

License

Copyright 2026 TrianaLab.

Licensed under the MIT License.

About

Kubernetes operator for Pacto service contract validation at runtime

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages