Skip to main content
Redhat Developers  Logo
  • AI

    Get started with AI

    • Red Hat AI
      Accelerate the development and deployment of enterprise AI solutions.
    • AI learning hub
      Explore learning materials and tools, organized by task.
    • AI interactive demos
      Click through scenarios with Red Hat AI, including training LLMs and more.
    • AI/ML learning paths
      Expand your OpenShift AI knowledge using these learning resources.
    • AI quickstarts
      Focused AI use cases designed for fast deployment on Red Hat AI platforms.
    • No-cost AI training
      Foundational Red Hat AI training.

    Featured resources

    • OpenShift AI learning
    • Open source AI for developers
    • AI product application development
    • Open source-powered AI/ML for hybrid cloud
    • AI and Node.js cheat sheet

    Red Hat AI Factory with NVIDIA

    • Red Hat AI Factory with NVIDIA is a co-engineered, enterprise-grade AI solution for building, deploying, and managing AI at scale across hybrid cloud environments.
    • Explore the solution
  • Learn

    Self-guided

    • Documentation
      Find answers, get step-by-step guidance, and learn how to use Red Hat products.
    • Learning paths
      Explore curated walkthroughs for common development tasks.
    • Guided learning
      Receive custom learning paths powered by our AI assistant.
    • See all learning

    Hands-on

    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.
    • Interactive labs
      Learn by doing in these hands-on, browser-based experiences.
    • Interactive demos
      Click through product features in these guided tours.

    Browse by topic

    • AI/ML
    • Automation
    • Java
    • Kubernetes
    • Linux
    • See all topics

    Training & certifications

    • Courses and exams
    • Certifications
    • Skills assessments
    • Red Hat Academy
    • Learning subscription
    • Explore training
  • Build

    Get started

    • Red Hat build of Podman Desktop
      A downloadable, local development hub to experiment with our products and builds.
    • Developer Sandbox
      Spin up Red Hat's products and technologies without setup or configuration.

    Download products

    • Access product downloads to start building and testing right away.
    • Red Hat Enterprise Linux
    • Red Hat AI
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Featured

    • Red Hat build of OpenJDK
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat Developer Toolset

    References

    • E-books
    • Documentation
    • Cheat sheets
    • Architecture center
  • Community

    Get involved

    • Events
    • Live AI events
    • Red Hat Summit
    • Red Hat Accelerators
    • Community discussions

    Follow along

    • Articles & blogs
    • Developer newsletter
    • Videos
    • Github

    Get help

    • Customer service
    • Customer support
    • Regional contacts
    • Find a partner

    Join the Red Hat Developer program

    • Download Red Hat products and project builds, access support documentation, learning content, and more.
    • Explore the benefits

Why killing pods is not enough: Testing operator reconciliation with operator-chaos

Chaos testing for Kubernetes operators with operator-chaos

July 11, 2026
Ugo Giordano
Related topics:
Application development and deliveryContainersGo
Related products:
Red Hat OpenShift

    This is the first article of a two-part series on operator-chaos, an open source chaos engineering framework for Kubernetes operators. In this series, we discuss why traditional chaos tools miss the most dangerous class of operator failures, introduce the four injection modes that operator-chaos provides, and run it against cert-manager on a live cluster. In the next article, we show what happens when operators fail these tests.

    Your chaos tests are passing. Your operators are not resilient.

    Here's a scenario that might feel familiar. You have a Kubernetes operator running in production. You are diligent about resilience testing, so you run Krkn or Litmus against it. Kill pods. Block network traffic. Exhaust resource quotas. Everything passes. Green across the board. You ship with confidence.

    Then something mundane happens. Someone accidentally scales your controller Deployment to zero replicas. A brief API server blip disconnects your controller's watch stream. A colleague deletes a Service that your operator created, thinking it was left over from a test. And your operator never recovers. Users start reporting errors hours later. The Deployment is sitting at zero replicas, the Service is gone, and your operator's reconciliation loop is doing nothing about it.

    Why? Because killing pods and watching them restart only tests one thing: The Kubernetes deployment controller. It tells you nothing about whether your operator will restore its full desired state when something drifts.

    We built operator-chaos to find these gaps. It is an open source Go framework (requiring Go 1.26+) that tests Kubernetes operator reconciliation with 20 failure injection types across four categories, four injection modes ranging from black-box command-line interface (CLI) to per-action interceptors, and structured verdicts that tell you exactly what your operator did (or did not do) when things went wrong.

    What makes operator reconciliation different

    A Kubernetes operator does not just manage pods. It manages a resource graph: Deployments, Services, ConfigMaps, Secrets, RoleBindings, Leases, ValidatingWebhookConfigurations, and CustomResourceDefinitions. The reconciliation loop is supposed to enforce the entire desired state of that graph, not just check whether individual resources exist.

    Traditional chaos tools ask a single question, illustrated in figure 1: Does the pod come back?

    Traditional chaos compared to operator-chaos.
    Figure 1: Traditional chaos compared to operator-chaos.

    That question has a simple answer, and it is almost always "yes" because the Kubernetes Deployment controller handles pod restarts. You are testing built-in Kubernetes functionality, not your operator's reconciliation logic.

    The harder questions are the ones that expose real production failures:

    • If someone scales your Deployment to zero replicas, does the operator detect it and restore the intended replica count?
    • If a Service gets deleted, does the operator recreate it?
    • If a container image gets corrupted (triggering ImagePullBackOff), does the operator detect the stuck rollout and fix it?
    • If the leader election Lease gets deleted, does the operator re-establish leadership?

    These are not hypothetical scenarios. They are things that happen in real clusters, caused by human error, automation bugs, or other controllers competing for the same resources. And most operators silently fail when they happen.

    Knowledge models: Teaching operator-chaos what your operator manages

    To understand what your operator manages, operator-chaos uses knowledge models. A knowledge model is a YAML file that declares the operator's namespace, its components, the resources each component creates, and what "healthy" looks like. This lets the framework inject failures that are semantically meaningful to your operator, not just generic infrastructure disruptions. Figure 2 displays a typical knowledge model workflow.

    Figure 2: How a knowledge model describes your operator.
    Figure 2: How a knowledge model describes your operator.

    Here is a trimmed example for cert-manager:

    operator:
      name: cert-manager
      namespace: cert-manager
    
    components:
      - name: cert-manager-controller
        controller: cert-manager-operator
        managedResources:
          - apiVersion: apps/v1
            kind: Deployment
            name: cert-manager
            namespace: cert-manager
            labels:
              app.kubernetes.io/name: cert-manager
            expectedSpec:
              replicas: 1
        steadyState:
          checks:
            - type: conditionTrue
              apiVersion: apps/v1
              kind: Deployment
              name: cert-manager
              namespace: cert-manager
              conditionType: Available
          timeout: "60s"
    
    recovery:
      reconcileTimeout: "300s"
      maxReconcileCycles: 10

    Three components (controller, webhook, CA injector), each with a managed Deployment and a steady-state check looking for Available=True. Writing this took about 10 minutes.

    Twenty failure types across four categories

    The framework ships with twenty failure injection types, organized into four categories:

    • Infrastructure failures test basic recovery: PodKill, NetworkPartition, QuotaExhaustion, NamespaceDeletion, DeploymentScaleZero.
    • Configuration failures test spec drift detection: ConfigDrift, CRDMutation, LabelStomping, ClientFault, ImageCorrupt, CrashLoopInject.
    • Access control failures test permission and webhook resilience: RBACRevoke, WebhookDisrupt, WebhookLatency.
    • Lifecycle failures test resource graph integrity: FinalizerBlock, OwnerRefOrphan, SecretDeletion, ResourceDeletion, LeaderElectionDisrupt, PDBBlock.

    Every experiment produces a structured verdict, not just pass/fail:

    • Resilient: The operator restored the full desired state within the recovery timeout.
    • Degraded: The operator is running, but it did not correct the drift.
    • Failed: The operator crashed, stopped reconciling, or entered an unrecoverable state.
    • Inconclusive: The pre-check failed, or the test could not determine the outcome.

    The experiment lifecycle

    Every experiment follows the same five-phase lifecycle, regardless of injection type. Figure 3 displays the phases of an experiment's lifecycle.

    The experiment lifecycle.
    Figure 3: The experiment lifecycle.
    1. Steady-state pre-check: Before injecting anything, operator-chaos verifies the operator is healthy.
    2. Failure injection: The framework injects a specific, operator-semantic failure.
    3. Observation: The framework watches for recovery, tracking reconciliation cycles, resource state changes, and timing.
    4. Steady-state post-check: The framework runs the same health checks from Phase 1 to see if the operator restored the desired state.
    5. Verdict evaluation: Based on the evidence, the framework assigns a structured verdict.

    Cleanup happens automatically after the verdict, whether the experiment passed or failed.

    Four injection modes

    This is the core architectural decision in operator-chaos. We provide four injection modes, each targeting a different layer of the operator's stack.

    Mode 1: CLI (ChaosExperiment CRs)

    External, black-box, zero code changes. You write ChaosExperiment YAML files, point them at a running operator on a live cluster, and get verdicts.

    operator-chaos run experiments/cert-manager/deployment-scale-zero.yaml 
      --knowledge knowledge/cert-manager.yaml -v
    [PRE-CHECK] Steady state verified (1/1 checks passed)
    [INJECT]    DeploymentScaleZero: scaled Deployment cert-manager to 0 replicas
    [OBSERVE]   Deployment cert-manager: replicas restored to 1 (2.1s after injection)
    [OBSERVE]   Deployment cert-manager: Available=True (5.3s after injection)
    [POST-CHECK] Steady state verified (1/1 checks passed)
    [VERDICT]   Resilient
                Recovery time: 5.3s

    When to use it: First pass on any operator. CI/CD gates. No code changes needed.

    Figure 4 provides an overview of how operator-chaos injects failures in this mode.

    How operator-chaos injects failures in CLI mode.
    Figure 4: How operator-chaos injects failures in CLI mode.

    Mode 2: Transport-level (chaostransport)

    Wraps http.RoundTripper with rest.Config.WrapTransport, intercepting every HTTP request the operator makes to the API server. Integration is three lines:

    cfg := ctrl.GetConfigOrDie()
    ct := chaostransport.NewChaosTransport(chaostransport.NewFaultConfig(nil))
    cfg.WrapTransport = ct.WrapTransport

    When to use it: Testing how the operator handles API server failures that affect informers, watches, and the full HTTP path.

    Figure 5 demonstrates how ChaosTransport intercepts HTTP requests.

    How ChaosTransport intercepts HTTP requests.
    Figure 5: How ChaosTransport intercepts HTTP requests.

    Mode 3: Client-level (ChaosClient)

    Wraps client.Client and intercepts CRUD operations. More targeted than transport-level, you can fail specific operations without affecting leader election and health probes.

    faults := sdk.NewFaultConfig(map[sdk.Operation]sdk.FaultSpec{
        sdk.OpUpdate: {ErrorRate: 0.5, Error: "update conflict"},
    })
    cc := chaosclient.NewChaosClient(realClient, faults)

    When to use it: Testing how reconcilers handle specific API operation failures without breaking infrastructure.

    Mode 4: Action pipeline (ActionInterceptor)

    In this mode, the finest details are possible. Instead of "fail all updates" you say "fail the deploy action 50% of the time" or "skip the garbage collection action entirely."

    interceptor := chaostransport.NewActionInterceptor(map[string]chaostransport.ActionFaultConfig{
        "deploy": {FailBefore: "simulated deploy failure", ErrorRate: 0.5},
        "gc":     {Skip: true},
    })

    When to use it: Testing specific reconciliation steps in isolation. Simulating partial failures in multi-step pipelines.

    Comparing the four modes

    The four injection modes each targets a different layer, as summarized in figure 6.

    The four injection modes each targets a different layer
    Figure 6: The four injection modes each targets a different layer

    Here's a summary of what each mode does best:

    ModeLayerCode changesCatches informer faultsPer-action targetingBest for
    CLIExternalNoneN/A (real mutations)NoFirst pass, CI/CD
    TransportHTTP3 linesYesNoFull-stack API faults
    Clientclient.Clientapproximately 5 linesNoNoTargeted CRUD faults
    ActionReconciler actionsPer-actionNoYesPipeline step isolation

    The chaostransport zero-dependency module

    Operators in the OpenDataHub ecosystem are pinned to different controller-runtime and k8s.io versions. If the chaos SDK imports controller-runtime v0.20, none of those operators can go get it without a major dependency upgrade.

    The chaostransport sub-module solves this. Its go.mod is exactly two directives:

    module github.com/opendatahub-io/operator-chaos/pkg/chaostransport
    go 1.22.0

    Zero dependencies on controller-runtime or any k8s.io library. Any operator can import it:

    go get github.com/opendatahub-io/operator-chaos/pkg/chaostransport

    No version conflicts. No transitive dependency hell.

    ConfigMap-driven runtime control

    Both the SDK and the chaostransport module support dynamic fault configuration using a ConfigMap called operator-chaos-config. The chaostransport module provides a ParseFaultConfigFromData helper that parses fault configuration from a ConfigMap's data map. Operators implement a simple watcher goroutine (typically 10-15 lines) that reads the ConfigMap periodically and calls UpdateFaultConfig to swap the configuration atomically.

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: operator-chaos-config
      namespace: my-operator
    data:
      config: |
        {
          "active": true,
          "faults": {
            "update": {"errorRate": 0.3, "error": "chaos conflict"},
            "get":    {"errorRate": 0.1, "error": "chaos unavailable"}
          }
        }

    Figure 7 provides an overview of ConfigMap-driven fault control. Deploy the instrumented operator, wait for steady state, apply the ConfigMap. The watcher picks it up within 10 seconds. When done, delete the ConfigMap or set active to false.

    ConfigMap-driven fault control.
    Figure 7: ConfigMap-driven fault control.

    Live demo: cert-manager passing 18/18

    In this demonstration, you can see that the cert-manager-operator actively reconciles the Deployment specs. When any drift occurs, the managing controller detects the change and corrects it. Recovery times are consistently under 5 seconds because watches trigger immediate reconciliation.

    operator-chaos suite experiments/cert-manager/ 
      --knowledge knowledge/cert-manager.yaml --recursive --report-dir results/
    Running 18 experiments from experiments/cert-manager/
    
      [1/18]  cert-manager-controller-pod-kill .................. Resilient (3.4s)
      [2/18]  cert-manager-controller-network-partition ......... Resilient (8.2s)
      [3/18]  cert-manager-controller-label-stomping ............ Resilient (2.1s)
      [4/18]  cert-manager-controller-quota-exhaustion .......... Resilient (4.7s)
      [5/18]  cert-manager-controller-rbac-revoke ............... Resilient (6.3s)
      [6/18]  cert-manager-webhook-pod-kill ..................... Resilient (3.1s)
      [7/18]  cert-manager-webhook-network-partition ............ Resilient (7.9s)
      [8/18]  cert-manager-webhook-label-stomping ............... Resilient (1.8s)
      [9/18]  cert-manager-webhook-quota-exhaustion ............. Resilient (4.2s)
      [10/18] cert-manager-webhook-webhook-cert-corrupt ......... Resilient (5.6s)
      [11/18] cert-manager-cainjector-pod-kill .................. Resilient (2.9s)
      [12/18] cert-manager-cainjector-network-partition ......... Resilient (8.4s)
      [13/18] cert-manager-cainjector-label-stomping ............ Resilient (1.9s)
      [14/18] cert-manager-cainjector-quota-exhaustion .......... Resilient (3.8s)
      [15/18] cert-manager-image-corrupt ....................... Resilient (12.4s)
      [16/18] cert-manager-crashloop-inject .................... Resilient (11.7s)
      [17/18] cert-manager-resource-deletion-service ........... Resilient (4.1s)
      [18/18] cert-manager-pdb-block ........................... Resilient (2.3s)
    
    Suite complete: 18/18 Resilient, 0 Degraded, 0 Failed

    Fuzz testing harness

    The pkg/sdk/fuzz/ package provides a fuzz testing harness that integrates with Go's native testing.F fuzzer. It generates random FaultConfig values, wires them into a ChaosClient, constructs a fresh reconciler, runs reconciliation, and checks invariants.

    func FuzzMyReconciler(f *testing.F) {
        f.Add(uint16(0x01FF), uint8(0), uint16(32768))
        f.Fuzz(func(t *testing.T, opMask uint16, faultType uint8, intensity uint16) {
            h := fuzz.NewHarness(myFactory, myScheme, myRequest, seedObjects...)
            h.AddInvariant(fuzz.ObjectExists(key, &myv1.MyResource{}))
            fc := fuzz.DecodeFaultConfig(opMask, faultType, intensity)
            if err := h.Run(t, fc); err != nil {
                t.Fatal(err)
            }
        })
    }

    The harness catches panics, distinguishes chaos-injected errors from real errors, and verifies post-conditions. Particularly good at finding edge cases like "Get fails but Create succeeds, then Delete fails."

    Get started with operator-chaos in 5 minutes

    You can start using operator-chaos today.

    Step 1: Install

    First, install the latest release.

    go install github.com/opendatahub-io/operator-chaos/cmd/operator-chaos@latest

    Step 2: Knowledge model

    Write a minimal knowledge model, replacing names with your operator's values.

    Step 3: Run preflight

    Run a preflight check:

    operator-chaos preflight --knowledge knowledge.yaml -v

    Step 4: Experiment

    Generate and run your first experiment:

    operator-chaos init --component controller --operator my-operator --type PodKill > experiment.yaml
    operator-chaos run experiment.yaml --knowledge knowledge.yaml -v

    Step 5: Find bugs

    Try the experiments that actually find bugs:

    operator-chaos init --component controller --operator my-operator --type DeploymentScaleZero > scale-zero.yaml
    operator-chaos run scale-zero.yaml --knowledge knowledge.yaml -v

    Step 6: Add SDK-level chaos (optional)

    Optionally, you can add extra chaos to your tests:

    go get github.com/opendatahub-io/operator-chaos/pkg/chaostransport

    Then add three lines to your operator's main().

    Tips and best practices

    • Start with CLI mode: Run the full suite before writing any SDK integration code.
    • Tier your experiments: PodKill (Tier 1) should always pass. DeploymentScaleZero (Tier 3) is where real bugs hide.
    • Use the zero-dependency module: If your operator imports any version of controller-runtime, use chaostransport to avoid dependency conflicts.
    • Watch your own infrastructure, not just your CRDs: Set up informers for every resource type your operator manages.
    • Use server-side apply: It simplifies spec enforcement. Operators that use server-side apply consistently pass more experiments.
    • Test with ConfigMap-driven faults before hardcoding: Deploy, toggle with ConfigMap, observe.
    • Add fuzz tests for critical reconcilers: Even 30 seconds of fuzzing covers more failure combinations than a hand-written test suite.

    Conclusion

    PodKill tells you how the Kubernetes Deployment controller works. DeploymentScaleZero tells you whether your operator works. They are testing completely different things, and most chaos test suites only run the first one. The operator-chaos project gives you four injection modes to test at every layer, from external black-box mutations all the way down to individual reconciler actions.

    Not every operator passes. In the next article, we reveal what happens when operators fail these tests, and what the failure patterns tell us about reconciliation design.

    Learn more

    • Documentation
    • GitHub
    • Full experiment library (22 operators, 429 experiments)
    • Red Hat Developer Sandbox (try it without installing anything)

    Related Posts

    • Type what you want to break: AI-assisted chaos engineering with Krkn

    • Unleash controlled chaos with krknctl

    • 5 anti-patterns that cause Kubernetes operator vulnerabilities

    • Protect your Kubernetes Operator from OOMKill

    • Modern Kubernetes monitoring: Metrics, tools, and AIOps

    Recent Posts

    • New features in Python 3.14

    • Why killing pods is not enough: Testing operator reconciliation with operator-chaos

    • Troubleshoot Red Hat OpenShift Virtualization localnet with the netobserv command

    • EvalHub: Capability and safety benchmarking for AI models

    • Tune and troubleshoot Red Hat Data Grid cross-site replication

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Platforms

    • Red Hat AI
    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform
    • See all products

    Build

    • Developer Sandbox
    • Developer tools
    • Interactive tutorials
    • API catalog

    Quicklinks

    • Learning resources
    • E-books
    • Cheat sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site status dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2026 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Chat Support

    Please log in with your Red Hat account to access chat support.