The cluster-etcd-operator (CEO) manages the etcd cluster in OpenShift, handling:
- etcd cluster scaling during bootstrap and steady state operation
- TLS certificate provisioning and rotation
- Backup and disaster recovery
- Static pod lifecycle management via library-go framework
- etcd health monitoring and defragmentation
- Bootstrap teardown and scaling strategies (HA, Delayed-HA, Two-Node, etc.)
See ARCHITECTURE.md for detailed architecture documentation.
- Never edit
vendor/files - managed bygo mod vendor - Never edit generated files - anything in
vendor/or matchingzz_generated.*patterns - Always run
make verifybefore committing - includes linting, generated file checks, and vendor verification - Always run
go mod tidy && go mod vendorafter dependency changes - Never modify manifests without regenerating - alerts/dashboards are generated from jsonnet (see Alerts and Dashboards)
- Always use
make buildnotgo build- for proper version injection
cluster-etcd-operator/
├── bindata/ # Embedded YAML manifests and scripts
│ ├── bootkube/ # Bootstrap manifests
│ ├── etcd/ # etcd manifests and scripts (cluster backup, cluster restore, quorum restore)
│ └── tnfdeployment/ # Two Node Fencing (TNF) manifests
├── cmd/
│ ├── cluster-etcd-operator/ # Main operator binary
│ ├── cluster-etcd-operator-tests-ext/ # OTE test binary
│ ├── tnf-monitor/ # Two Node Fencing monitor
│ └── tnf-setup-runner/ # Two Node Fencing setup
├── docs/ # Documentation (HACKING, FAQ, overview)
├── hack/ # Build and generation scripts
├── jsonnet/ # Alert and dashboard definitions
├── manifests/ # CVO-managed YAML (deployment, RBAC, monitoring)
├── pkg/
│ ├── operator/ # Core operator logic (20+ controllers)
│ ├── etcdcli/ # etcd client wrapper
│ ├── tlshelpers/ # TLS certificate utilities
│ └── dnshelpers/ # DNS resolution helpers
└── test/e2e/ # End-to-end tests
Controllers follow the library-go factory pattern:
func NewMyController(
livenessChecker *health.MultiAlivenessChecker,
operatorClient v1helpers.StaticPodOperatorClient,
eventRecorder events.Recorder,
) factory.Controller {
c := &MyController{
operatorClient: operatorClient,
}
syncer := health.NewDefaultCheckingSyncWrapper(c.sync)
livenessChecker.Add("MyController", syncer)
return factory.New().
ResyncEvery(time.Minute).
WithInformers(
operatorClient.Informer(),
).
WithSync(syncer.Sync).
ToController("MyController", eventRecorder.WithComponentSuffix("my-controller"))
}Key elements:
- Constructor function named
NewXxxController - Wraps sync function with health checker
- Uses library-go factory builder
- Returns
factory.Controllerinterface - All resources the controller reads must have a corresponding informer registered via
.WithInformers()— missing informers are one of the most common bugs in this codebase. When adding or modifying a controller, verify that every lister/getter used in the sync function has its informer wired up in the factory builder, otherwise the controller will operate on stale data
Use v1helpers.UpdateStatus to set conditions:
_, _, updateErr := v1helpers.UpdateStatus(ctx, c.operatorClient,
v1helpers.UpdateConditionFn(operatorv1.OperatorCondition{
Type: "MyControllerDegraded",
Status: operatorv1.ConditionTrue,
Reason: "SyncError",
Message: err.Error(),
}),
)Guidelines for status conditions:
- Use
Degradedvery sparingly — only set it after a meaningful amount of time has passed (etcd moves slowly due to static pod machinery) and only when it requires actionable human intervention - Avoid conditions that flap — always add an inertia component (e.g., require the error state to persist for several sync cycles before reporting Degraded). The operator has historically bumped inertia from 5 to 10 minutes to reduce flapping during rollouts.
Openshift supports anywhere from 1 to 5 control-plane nodes depending on the level of high availability needed. The bootstrap scaling strategy describes the invariants which will be enforced when scaling the etcd cluster.
The operator enforces different quorum requirements during bootstrap:
HAScalingStrategy- requires at least 3 nodes to scale up (default)DelayedHAScalingStrategy- allows 2 nodes during bootstrap, then requires 3TwoNodeScalingStrategy- requires 2 nodes (for Two Node OpenShift with Fencing deployments)DelayedTwoNodeScalingStrategy- allows 1 node during bootstrap, then requires 2 (for Two Node OpenShift with Fencing deployments)BootstrapInPlaceStrategy- used with assisted installer where the bootstrap node is repurposed as the final master node, allowing a 3-node cluster to be deployed with only 3 total machines (no separate bootstrap node). The bootstrap node performs a live ISO pivot to become a control-plane member. Particularly useful for bare metal installsUnsafeScalingStrategy- scaling without regard to node count or quorum
Check pkg/operator/ceohelpers/bootstrap.go for implementation.
- DNS Independence - The
EtcdEndpointsControllermust never depend on DNS (directly or transitively) as it maintains the IP-based ConfigMap that DNS depends on. DNS has no guarantee that a single static IP is returned. Previously, member nodes confused their identity by alternating between IPs. Using only IPs is a core design choice in this operator - CVO Resource Management - Resources in
manifests/are managed by CVO; removal requires special handling (see CVO Resource Removal). The numeric ordering of file names inmanifests/determines CVO deployment order — incorrect ordering has caused outages in CI (e.g., when a manifest was deployed before its dependency) - Never Remove Controllers - An agent should never remove a controller. Controller removals are extremely rare (only one in the history of the project) and require cleaning up stale status conditions to prevent upgrade blockers. This is a human decision
- Static Pod Revisions - Follow library-go static pod revision pattern; don't invent custom update logic
- Namespace - Primary namespace for the operator resources is
openshift-etcd-operator; target namespace for the operand isopenshift-etcd. Constants are found inpkg/operator/operatorclient/interfaces.goand should always be used
# Build all binaries (operator + test extension)
make build
# Run unit tests
make test-unit
# Run e2e tests (requires OpenShift cluster)
make test-e2e
# Verify linting, generated files, vendor
make verify
# Update dependencies
go mod tidy && go mod vendorWhen debugging live systems, use commands in profiling and debugging.
To run CEO locally against a cluster:
# Build executable
make build
# Get config
kubectl -n openshift-etcd-operator get configmap etcd-operator-config -o jsonpath='{.data.config\.yaml}' > ./config.yaml
# Scale down cluster-etcd-operator deployment in cluster and exclude from CVO
./hack/unmanage.sh
# "Missing Version" markers
export OPERATOR_IMAGE_VERSION=0.0.1-snapshot
export OPERAND_IMAGE_VERSION=0.0.1-snapshot
# Run local operator
./cluster-etcd-operator \
--config ./config.yaml
--namespace openshift-etcd-operator \
--kubeconfig /path/to/kubeconfigThis repo uses OpenShift Tests Extension (OTE):
# Build test binary
make build # produces cluster-etcd-operator-tests-ext
# List test suites
./cluster-etcd-operator-tests-ext list suites
# Run specific suite with concurrency
./cluster-etcd-operator-tests-ext run-suite openshift/cluster-etcd-operator/all -c 1
# Run with JUnit output
./cluster-etcd-operator-tests-ext run-suite openshift/cluster-etcd-operator/all \
--junit-path=/tmp/junit-results/junit.xmlTest files are in test/e2e/ and registered via the OTE framework in cmd/cluster-etcd-operator-tests-ext/.
Prometheus alerts and Grafana dashboards are generated from jsonnet, not hand-edited. In general, avoid adding new alerts — they are difficult to maintain in CI and we add them very rarely.
Almost all files in the jsonnet/ directory can be changed, but the lock file (jsonnetfile.lock.json) and vendor/ folder are off limits.
main.jsonnet- Imports upstream etcd mixin alerts and merges with custom overrides. Contains theexcludedAlertslist for upstream alerts we replace or disable. The exclusion list must be in sorted order — the comparison uses a sort-dedupe, so unsorted entries will silently breakcustom.libsonnet- Used for overrides of upstream alerts and addition of new downstream-specific alertsdashboard.jsonnet- Dashboard definitions. Custom dashboards/panels should be their own dedicated files (e.g.,cpu_iowait_dashboard.json) and merge-appended intodashboard.jsonnet
- If changing upstream alerts (e.g., different thresholds): update upstream alerts first, then exclude the original in
main.jsonnetand add the override incustom.libsonnet - For new downstream alerts: add them in
custom.libsonnet- New alerts are added only very rarely. They should be avoided unless absolutely necessary
- Regenerate manifests:
hack/generate.sh - This updates:
manifests/0000_90_etcd-operator_03_prometheusrule.yamlmanifests/0000_90_cluster-etcd-operator_01-dashboards.yaml
- Verify the alert is reflected correctly in the generated output — always diff the generated YAML to confirm your changes took effect
Never edit the generated YAML files directly.
Requires: jsonnet, jb (jsonnet-bundler), gojsontoyaml
See https://github.com/google/jsonnet
Resources in manifests/ are managed by CVO. To remove one:
- Don't just delete the manifest file - it won't be removed from clusters
- Add the
release.openshift.io/delete: "true"annotation to the resource - CVO will garbage collect it on next update
- Keep the annotation in manifests for 1 release, then remove the file
See:
- https://github.com/openshift/enhancements/blob/master/enhancements/update/object-removal-manifest-annotation.md
- #1016
- Missing or wrong informer scope — Always verify informer scope matches what the controller actually needs, and wait for caches to sync (e.g. using the general node lister instead of master-only node lister caused controllers to see non-master nodes and make incorrect scaling decisions)
- Insufficient inertia on status conditions — controllers reacting too quickly to transient state changes cause flapping
- Bootstrap member management and quorum — non-deterministic IP selection, revision stability vs. member removal timing, and quorum safety during member add/remove are the single most-fixed area. Changes here interact differently across release branches
- Health check sensitivity — Liveness probe timeouts and failure thresholds need careful tuning. Too aggressive kills healthy pods. Too relaxed misses real failures
- Shell script fragility —
cluster-backup.shandcluster-restore.shhave been sources of repeated bugs (caching issues, path errors, retention logic). Changes to these scripts need extra scrutiny - Static pod revision tracking — The static pod revision mechanism is the critical path for config rollout. Getting the balance wrong between "wait for stability" and "don't block bootstrap" causes cascading issues. Each release branch has subtly different behavior, making backports risky
- Upgrade path considerations — resources and conditions from N-1 releases may still exist. Never assume a clean slate
TLS certificates are managed by the etcdcertsigner and etcdcertcleaner controllers. Cipher suite configuration is a known pain point:
- Bootstrap ciphers are defined in
pkg/cmd/render/env.gobased on theTLSProfileIntermediateTypeprofile fromconfigv1.TLSProfiles - Runtime ciphers are sourced from
observedConfiginpkg/etcdenvvar/etcd_env.goand filtered throughtlshelpers.SupportedEtcdCiphers()which validates each cipher against what etcd actually supports - Not all OpenShift ciphers are supported by etcd —
SupportedEtcdCiphers()inpkg/tlshelpers/tlshelpers.gofilters out unsupported ciphers usingtlsutil.GetCipherSuite(). When OpenShift adds new ciphers, they may silently be dropped if etcd doesn't support them - When modifying cipher handling, verify that the resulting cipher list is non-empty and that both bootstrap and runtime paths produce consistent results
The operator supports an escape-hatch mechanism via UnsupportedConfigOverrides on the operator spec. This is used for unsupported, non-production overrides (e.g., useUnsupportedUnsafeNonHANonProductionUnstableEtcd). See pkg/operator/ceohelpers/unsupported_override.go.
This mechanism exists because the operator historically had severe stability problems and needed a way to override behavior without a full release cycle. For new feature development, prefer using proper feature gates instead of adding new unsupported config keys.
Fencing scripts in bindata/etcd/ (shell scripts) and bindata/tnfdeployment/ (deployment manifests) should be reviewed for injection vulnerabilities when modified.
pkg/operator/starter.go- Operator entrypoint, initializes all controllerspkg/operator/ceohelpers/bootstrap.go- Bootstrap scaling logicpkg/operator/etcdendpointscontroller/- Critical DNS-independent controllerpkg/etcdcli/etcdcli.go- etcd client abstractionbindata/etcd/cluster-backup.sh- Backup scriptbindata/etcd/cluster-restore.sh- Restore scriptdocs/HACKING.md- Maintenance procedures