|
| 1 | +# ZTWIM Development Guide |
| 2 | + |
| 3 | +> For generic Go/operator practices, see the Platform Development Guide. |
| 4 | +
|
| 5 | +## Quick Start |
| 6 | + |
| 7 | +### Prerequisites |
| 8 | + |
| 9 | +| Tool | Version | Notes | |
| 10 | +|------|---------|-------| |
| 11 | +| Go | 1.25+ | See `go.mod` for exact version (1.25.7) | |
| 12 | +| Docker or Podman | Latest | Set `CONTAINER_TOOL=podman` if using Podman | |
| 13 | +| oc / kubectl | Latest | For cluster interaction | |
| 14 | +| OpenShift | 4.18+ | E2E tests require a live cluster | |
| 15 | +| operator-sdk | v1.39.0 | Auto-downloaded by Makefile | |
| 16 | + |
| 17 | +### Build Commands |
| 18 | + |
| 19 | +```bash |
| 20 | +make build # Full build: manifests + generate + fmt + vet + binary |
| 21 | +make build-operator # Binary only (no codegen, fastest iteration) |
| 22 | +make test # Unit tests with coverage |
| 23 | +make verify # vet + fmt + lint |
| 24 | +make lint # golangci-lint (v1.59.1) |
| 25 | +``` |
| 26 | + |
| 27 | +## Code Style |
| 28 | + |
| 29 | +### Import Order |
| 30 | + |
| 31 | +Imports follow this grouping convention (checked by `goimports` linter in CI): |
| 32 | + |
| 33 | +1. Standard library |
| 34 | +2. `k8s.io/*`, `sigs.k8s.io/*` |
| 35 | +3. Third-party (`github.com/go-logr/logr`, `github.com/operator-framework/api`, etc.) |
| 36 | +4. OpenShift (`github.com/openshift/api`, `github.com/openshift/client-go`) |
| 37 | +5. This project (`github.com/openshift/zero-trust-workload-identity-manager/...`) |
| 38 | + |
| 39 | +### Standard Import Aliases |
| 40 | + |
| 41 | +| Alias | Package | |
| 42 | +|---|---| |
| 43 | +| `ctrl` | `sigs.k8s.io/controller-runtime` | |
| 44 | +| `kerrors` | `k8s.io/apimachinery/pkg/api/errors` | |
| 45 | +| `apimeta` | `k8s.io/apimachinery/pkg/api/meta` | |
| 46 | +| `customClient` | `github.com/openshift/zero-trust-workload-identity-manager/pkg/client` | |
| 47 | +| `routev1` | `github.com/openshift/api/route/v1` | |
| 48 | + |
| 49 | +### File Headers |
| 50 | + |
| 51 | +All `.go` files must include the Apache 2.0 license header from `hack/boilerplate.go.txt`. |
| 52 | + |
| 53 | +## Development Workflow |
| 54 | + |
| 55 | +### Local Build |
| 56 | + |
| 57 | +```bash |
| 58 | +export OPERATOR_NAMESPACE=zero-trust-workload-identity-manager |
| 59 | +make build-operator |
| 60 | +./bin/zero-trust-workload-identity-manager --v=5 --metrics-secure=false |
| 61 | +``` |
| 62 | + |
| 63 | +The binary is FIPS-aware — `hack/go-fips.sh` sets `GOEXPERIMENT=strictfipsruntime` when the compiler supports it. Local builds without the FIPS-capable toolchain still succeed but emit a warning. |
| 64 | + |
| 65 | +### Testing on Cluster |
| 66 | + |
| 67 | +```bash |
| 68 | +make docker-build IMG=<registry>/ztwim:dev |
| 69 | +make docker-push IMG=<registry>/ztwim:dev |
| 70 | +make deploy IMG=<registry>/ztwim:dev |
| 71 | +``` |
| 72 | + |
| 73 | +Or the all-in-one: |
| 74 | + |
| 75 | +```bash |
| 76 | +make generate-deploy IMG=<registry>/ztwim:dev |
| 77 | +``` |
| 78 | + |
| 79 | +### Debugging |
| 80 | + |
| 81 | +Run locally against a remote cluster: |
| 82 | + |
| 83 | +```bash |
| 84 | +make run # starts controller with --v=5 against current kubeconfig |
| 85 | +``` |
| 86 | + |
| 87 | +Set `OPERATOR_NAMESPACE=zero-trust-workload-identity-manager` — the operator exits immediately if this is empty. |
| 88 | + |
| 89 | +## Common Tasks |
| 90 | + |
| 91 | +### Adding a New Operand |
| 92 | + |
| 93 | +1. Define the CRD type in `api/v1alpha1/<operand>_types.go` with `+kubebuilder:resource:scope=Cluster`, singleton CEL validation, `ConditionalStatus` embedding, and `GetConditionalStatus()` method. |
| 94 | +2. Create the controller package under `pkg/controller/<operand>/` following the reconciler structure: `Reconciler` struct, `New(mgr)`, `SetupWithManager(mgr)`, `Reconcile(ctx, req)`. |
| 95 | +3. Register the scheme and wire the controller in `cmd/zero-trust-workload-identity-manager/main.go`. |
| 96 | +4. Run `make manifests generate`. |
| 97 | +5. Add the new CR type to `cacheResourceWithoutReqSelectors` and `informerResources` in `pkg/client/client.go`. |
| 98 | +6. Add a `get<Operand>Status()` method to the ZTWIM controller and include it in `aggregateOperandStatus()`. |
| 99 | +7. Add a `Watches` entry for the new operand CR in the ZTWIM controller's `SetupWithManager()` with `operandStatusChangedPredicate`. |
| 100 | +8. Add RBAC markers on the ZTWIM controller for the new CRD, then `make manifests`. |
| 101 | +9. Add component label constant and label generator function in `pkg/controller/utils/labels.go`. |
| 102 | +10. Add image env var constant and getter in `pkg/controller/utils/constants.go` and `relatedImages.go`. |
| 103 | +11. Add bindata YAML under `bindata/<operand>/`, add constants, and run `make update-bindata`. |
| 104 | +12. Run `make manifests generate update-bindata && make verify && make test`. |
| 105 | + |
| 106 | +### Adding a New Bindata Resource |
| 107 | + |
| 108 | +1. Create the YAML manifest in `bindata/<component>/` with `app.kubernetes.io/managed-by: zero-trust-workload-identity-manager` label. |
| 109 | +2. Add an asset path constant in `pkg/controller/utils/constants.go`. |
| 110 | +3. Run `make update-bindata` to regenerate `pkg/operator/assets/bindata.go`. |
| 111 | +4. Add a `reconcileNewResource()` method following the decode → mutate → SetControllerReference → Get → Create/Update pattern. |
| 112 | +5. Call the new reconcile method in `Reconcile()` at the correct position in the ordering. |
| 113 | +6. Add a status condition constant for the new resource. |
| 114 | +7. Add a `Watches` entry in `SetupWithManager()` if it's a new GVK. |
| 115 | +8. Register the resource type in `NewCacheBuilder()` under `cacheResources` in `pkg/client/client.go`. |
| 116 | +9. Add the resource type to `ResourceNeedsUpdate()` in `pkg/controller/utils/` with field-level comparison. |
| 117 | +10. Run `make manifests generate update-bindata && make verify`. |
| 118 | + |
| 119 | +### Adding a New CRD Field |
| 120 | + |
| 121 | +1. Add the field to the appropriate `*Spec` or `*Status` struct in `api/v1alpha1/`. |
| 122 | +2. Add kubebuilder validation markers (Required/Optional, Enum, Pattern, Min/Max, Default). |
| 123 | +3. If immutable, add a CEL `XValidation` rule with `self == oldSelf`. |
| 124 | +4. Run `make generate` (deepcopy) then `make manifests` (CRD YAML). |
| 125 | +5. Update controller logic to read/use the new field. |
| 126 | +6. Update bindata templates if the field affects rendered YAML; run `make update-bindata`. |
| 127 | +7. Add unit tests covering validation (valid values, invalid values, immutability). |
| 128 | +8. Run `make verify` to confirm lint/fmt pass. |
| 129 | + |
| 130 | +### Updating Dependencies |
| 131 | + |
| 132 | +```bash |
| 133 | +go get <package>@<version> |
| 134 | +make vendor # runs go mod tidy + go mod vendor |
| 135 | +make verify # ensure lint and vet still pass |
| 136 | +``` |
| 137 | + |
| 138 | +## Build & Release |
| 139 | + |
| 140 | +### Local Build |
| 141 | + |
| 142 | +```bash |
| 143 | +make docker-build IMG=<registry>/ztwim:latest |
| 144 | +``` |
| 145 | + |
| 146 | +The `Dockerfile` uses a two-stage build: `registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.21` for compilation and `ubi9-minimal:9.4` as the runtime base. |
| 147 | + |
| 148 | +### CI Build |
| 149 | + |
| 150 | +`.ci-operator.yaml` defines the build root: |
| 151 | + |
| 152 | +```yaml |
| 153 | +build_root_image: |
| 154 | + name: builder |
| 155 | + namespace: ocp |
| 156 | + tag: rhel-9-golang-1.25-openshift-4.21 |
| 157 | +``` |
| 158 | +
|
| 159 | +CI runs `make test` (unit) and `make test-e2e` (E2E on a live cluster). |
| 160 | + |
| 161 | +### Release Process |
| 162 | + |
| 163 | +Release images are managed in the separate `zero-trust-workload-identity-manager-release` repository. The OLM bundle is generated via: |
| 164 | + |
| 165 | +```bash |
| 166 | +make bundle VERSION=<version> |
| 167 | +make bundle-build BUNDLE_IMG=<registry>/ztwim-bundle:v<version> |
| 168 | +``` |
| 169 | + |
| 170 | +## Common Mistakes |
| 171 | + |
| 172 | +1. **DO NOT** forget to set `OPERATOR_NAMESPACE` env var — the operator will exit(1) immediately. |
| 173 | +2. **DO NOT** add bindata YAML without running `make update-bindata` — the embedded assets won't include your changes. |
| 174 | +3. **DO NOT** run `make test` without `OPERATOR_NAMESPACE=zero-trust-workload-identity-manager` — tests rely on it for namespace resolution. |
| 175 | +4. **DO NOT** skip `make manifests generate` after changing `api/v1alpha1/` types — CRDs and DeepCopy will be stale. |
| 176 | +5. **DO NOT** use `controllerutil.SetControllerReference` without registering the owner type in the scheme — it will fail silently. |
| 177 | +6. **DO NOT** return `nil` from a reconciler when a sub-function errors — always propagate errors so controller-runtime can requeue. |
| 178 | +7. **DO NOT** create resources without setting owner references — orphaned resources won't be garbage-collected. |
| 179 | +8. **DO NOT** bypass the `FakeCustomCtrlClient` interface in tests — use counterfeiter fakes for consistent stubbing. |
| 180 | + |
| 181 | +## Logging Conventions |
| 182 | + |
| 183 | +| Scenario | Pattern | |
| 184 | +|---|---| |
| 185 | +| Error with requeue | `r.log.Error(err, "failed to <action>")` then `return (Result{}, err)` | |
| 186 | +| Error without requeue (validation) | `r.log.Error(err, "descriptive message", "key", value)` then set condition | |
| 187 | +| Informational not-found | `r.log.Info("resource not found, ignoring")` — never log at Error level | |
| 188 | +| Debug-level detail | `r.log.V(1).Info("skipping update", "reason", "...")` | |
| 189 | +| Status update failure in defer | `r.log.Error(err, "failed to update status")` — log only, do not propagate | |
| 190 | + |
| 191 | +Never use `fmt.Printf` or `klog` directly. |
| 192 | + |
| 193 | +## Event Recording |
| 194 | + |
| 195 | +Events are reserved for **user-actionable warnings** — not routine reconciliation. Rules: |
| 196 | +- Use `corev1.EventTypeWarning` for problems the user should address. |
| 197 | +- Do not emit events for transient API errors or routine reconciliation. |
| 198 | +- Event reason should be PascalCase (e.g., `TTLConfigurationWarning`). |
| 199 | + |
| 200 | +## Environment Variables |
| 201 | + |
| 202 | +| Variable | Purpose | Default | |
| 203 | +|----------|---------|---------| |
| 204 | +| `OPERATOR_NAMESPACE` | Namespace for operator resources | (required) | |
| 205 | +| `OPERATOR_CONDITION_NAME` | OLM OperatorCondition name for Upgradeable sync | (required) | |
| 206 | +| `CREATE_ONLY_MODE` | Skip updates, only create resources | `""` (disabled) | |
| 207 | +| `RELATED_IMAGE_SPIRE_SERVER` | Spire Server image override | Set by CSV | |
| 208 | +| `RELATED_IMAGE_SPIRE_AGENT` | Spire Agent image override | Set by CSV | |
| 209 | +| `RELATED_IMAGE_SPIFFE_CSI_DRIVER` | SPIFFE CSI Driver image override | Set by CSV | |
| 210 | +| `RELATED_IMAGE_SPIRE_OIDC_DISCOVERY_PROVIDER` | OIDC Discovery Provider image override | Set by CSV | |
| 211 | +| `RELATED_IMAGE_SPIRE_CONTROLLER_MANAGER` | Controller Manager image override | Set by CSV | |
| 212 | +| `RELATED_IMAGE_NODE_DRIVER_REGISTRAR` | Node Driver Registrar image override | Set by CSV | |
| 213 | +| `RELATED_IMAGE_SPIFFE_CSI_INIT_CONTAINER` | CSI init container image override | Set by CSV | |
| 214 | +| `HTTP_PROXY` | HTTP proxy for operand pods | `""` | |
| 215 | +| `HTTPS_PROXY` | HTTPS proxy for operand pods | `""` | |
| 216 | +| `TRUSTED_CA_BUNDLE_CONFIGMAP` | CA bundle ConfigMap (required when proxy set) | `""` | |
| 217 | +``` |
| 218 | + |
| 219 | +--- |
0 commit comments