Development Guideline
This page covers developing Kubedoop itself — the operators and the framework they share. For writing documentation see Document Writing Guidelines, and for the fork and pull request mechanics see First Contribution.
What you would be working on
Kubedoop is not one repository. Each piece lives under github.com/zncdatadev:
| Repository | Contains |
|---|---|
operator-go | The Go framework every operator builds on: shared CRD types, the reconciler, resource builders |
<product>-operator | One operator per product — zookeeper-operator, hdfs-operator, trino-operator, and so on |
commons-operator, listener-operator, secret-operator | The built-in operators every product cluster depends on |
containers | Product container images |
kubedoop-helm-charts | Published Helm charts |
docs | This site |
Each operator is an independent Go module that depends on operator-go. A change to shared
behaviour usually belongs in the framework; a change to one product's rendering belongs in that
product's operator.
Prerequisites
| Tool | Why |
|---|---|
| Go | Building and testing. The required version is in each repository's go.mod |
| Docker or Podman | Building operator images |
| kubectl | Talking to a cluster |
| kind | The local cluster end-to-end tests run against |
| Helm | Installing operator dependencies during e2e |
Everything else — controller-gen, kustomize, setup-envtest, golangci-lint, chainsaw — is
downloaded into the repository's bin/ directory by the Makefile on first use. Do not install those
globally; a mismatched version is a source of failures that do not reproduce in CI.
End-to-end tests run against the Kubernetes versions listed in
Kubernetes. Note that
KIND_K8S_VERSION in each Makefile is only the default for a local cluster and is older than what
CI exercises — CI overrides it with its own matrix.
Repository layout
Operators follow the standard Kubebuilder layout, with a few Kubedoop additions:
api/v1alpha1/ # CRD Go types — the API surface
internal/ # Reconcilers, builders, product-specific logic
cmd/ # Manager entrypoint
config/ # Kustomize manifests, including generated CRDs
deploy/helm/ # The operator's Helm chart
test/e2e/ # Chainsaw end-to-end suites
examples/ # Example custom resources
Makefile # Every task below
PROJECT # Kubebuilder project metadata
make help lists the targets in any operator repository.
The development loop
make manifests generate # regenerate CRDs and deepcopy code after API changes
make fmt vet # format and vet
make lint # golangci-lint
make test # unit tests
make build # build the manager binary
make run # run the controller against your current kubecontext
make test depends on manifests generate fmt vet, so it regenerates before running. If that
produces a diff, commit it — generated files are checked in.
Changing the API
API types live in api/v1alpha1/. After editing them:
make manifests generate
This regenerates the CRD YAML under config/ and the zz_generated.deepcopy.go files. Both are
committed, so a change to a type with no accompanying regeneration will show up as a diff in CI.
The Helm chart carries its own copy of the CRDs and the operator's RBAC rules, and they do not update themselves:
make helm-crd-sync # copy generated CRDs into deploy/helm/
make helm-rbac-sync # copy the generated ClusterRole rules into deploy/helm/
Forgetting these is the usual cause of a chart that installs an operator against a stale CRD.
Adding fields
Prefer the shared structures in operator-go's pkg/apis/commons/v1alpha1 — resources, logging,
affinity, PodDisruptionBudget — over defining a product-local equivalent. A field that ends up
copied into three operators is a sign it belongs in the framework instead.
Be careful with +kubebuilder:default on fields inside config. That block is folded from role to
role group, and structural defaulting fills a leaf as soon as its enclosing object exists — so a
default there makes "unset" indistinguishable from "explicitly this value", and the role's setting
can never win. Several fields in the framework carry comments explaining exactly this; defaults for
folded fields belong at consumption time.
Testing
Unit tests
make test
These run against envtest — a real API server and etcd, no scheduler or controllers. Enough to assert what the operator renders: that a given CR produces the StatefulSet, ConfigMap and Service you expect.
End-to-end tests
End-to-end tests use Chainsaw against a kind cluster. Cases
live in test/e2e/<suite>/<case>/, each a directory of YAML steps and assertions.
make setup-chainsaw-cluster # create the kind cluster, install operator dependencies
make setup-chainsaw-e2e # build the image, load it, deploy the operator
make chainsaw-e2e # run the suites
make cleanup-chainsaw-e2e # undeploy
setup-chainsaw-cluster installs the operators the product depends on — commons-operator,
listener-operator and secret-operator — from the published charts, because almost nothing works
without them: secrets, listeners and pod enrichment are all provided by those three.
Write an end-to-end case for anything that only shows up on a live cluster: rendered configuration a unit test would have to duplicate, rolling behaviour, discovery between two products. Keep assertions on the resources rather than on log output.
Working with operator-go
Operators depend on a released operator-go. When a change needs both the framework and an operator
at once, point the module at your local checkout while developing:
replace github.com/zncdatadev/operator-go => ../operator-go
Remove the replace before opening the pull request — it cannot be merged. Land the framework
change first, then bump the operator to the released version.
Before opening a pull request
Run the same gates CI does, from the operator's repository:
make manifests generate # and commit any resulting diff
make fmt vet lint
make test
make helm-crd-sync helm-rbac-sync # if the API changed
Then the end-to-end suite for anything touching rendering or reconcile behaviour.
Follow the commit and branch conventions in First Contribution, and describe in the pull request what you actually ran. A checklist box ticked without running the command is worse than an honest note about what was skipped.