Skip to content

feat: rewrite operator in Go (Kubebuilder v4)#298

Merged
mgrzybek merged 27 commits into
mainfrom
rewrite-go
Jul 13, 2026
Merged

feat: rewrite operator in Go (Kubebuilder v4)#298
mgrzybek merged 27 commits into
mainfrom
rewrite-go

Conversation

@mgrzybek

@mgrzybek mgrzybek commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Complete rewrite of the operator from Python/kopf to Go using Kubebuilder v4,
developed fully in TDD (Gherkin specs → implementation).

  • OpenStack client (internal/openstack/) — authentication via
    v3applicationcredential with custom CA support, catalog filtering by
    interface/region, purge of floating IPs, load balancers, security groups,
    Cinder volumes & snapshots, and application credentials; Cinder endpoint
    with volumev3 / block-storage / volume fallback chain; HTTP client
    timeout (30 s) as safety net
  • Controller (internal/controller/) — finalizer lifecycle, cluster name
    resolution from label or metadata.name, configurable volumes policy
    (delete/keep) via env var and per-cluster annotation, retry mechanism
    via annotation on purge failure, Prometheus counter
    capi_janitor_cleanups_total{result="success|failure"}, Kubernetes events
    CleanupSucceeded / CleanupFailed
  • Packaging — Go multi-stage Dockerfile (distroless/static:nonroot, UID
    65532); Nix-based reproducible OCI images (amd64 + arm64 cross-compiled)
    with CycloneDX SBOM via syft; CI updated to use nix-build + skopeo +
    docker manifest
  • Helm chartretryDefaultDelay value, liveness/readiness probes,
    updated RBAC (update verb on openstackclusters); 11 helm-unittest tests
  • 108 unit tests across 4 packages — no external cluster required

mgrzybek added 16 commits July 7, 2026 16:59
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- Tests for US1.1 (v3applicationcredential), US1.2 (interface/region),
  US1.3 (revoked credential / catalog 404), US1.4 (custom CA)
- Bug : s.userID not assigned in getToken
- Remplacing custom errors_as by errors.As stdlib
- Adding Session.HasEndpoint() for test introspection
- 24 tests, covering cloud.go 85–100%

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- resources_test.go (new): 13 tests covering US2.1 (filtering by
  cluster, description, multi-FIP) and US2.2 (successful deletion,
  transient 400/409 errors absorbed with verification, 500 error
  propagated, persistent FIPs → error, empty list → no verification,
  missing network endpoint → CatalogError)
- resources.go — DeleteFloatingIPs: moved `deleted = true` before the
  DELETE call so verification always triggers on transient errors;
  added explicit log for those errors

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- resources_test.go: 9 tests covering US3.1 (kube_service_<cluster>_
  prefix filtering), US3.2 (HTTP error on list → log + return nil, no
  exception propagated), and US3.3 (cascade=true, successful deletion,
  transient 400/409 absorbed with verification, 500 propagated,
  persistent LBs → error, empty list → no verification, missing
  load-balancer endpoint → skip)
- resources.go — DeleteLoadBalancers: moved deleted = true before
  DELETE call and added explicit warning log for transient errors
  (consistent with FIP cleanup pattern)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- resources_test.go: 6 tests covering US4.1 (description prefix/suffix
  filtering, different cluster, wrong prefix, unrelated description),
  US4.2 (successful deletion, transient HTTP 409 absorbed with
  verification, 500 propagated, persistent SG → error, nothing to
  delete → no verification)
- resources.go — DeleteSecurityGroups: moved deleted = true before
  DELETE call; added explicit warning log for transient errors
  (consistent with FIP/LB cleanup pattern)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- resources_test.go: 6 tests — description filter (prefix "Security Group
  for" + suffix "Service LoadBalancer in cluster <name>"), transient HTTP
  409 absorbed with verification triggered, HTTP 500 propagated,
  still-present → error, nothing-to-delete → no verification
- resources.go — DeleteSecurityGroups: moved deleted=true before DELETE
  call; added explicit warning log for transient errors

Epic 5 — Cinder Volumes (US5.1):
- resources_test.go: 6 tests — metadata filter
  (cinder.csi.openstack.org/cluster), keep annotation
  (janitor.capi.azimuth-cloud.com/keep=true) skips deletion, transient
  HTTP 409 absorbed with verification triggered, HTTP 500 propagated,
  still-present → error, nothing-to-delete → no verification
- resources.go — DeleteVolumes: aligned on FIP/LB/SG pattern (deleted=true
  before DELETE, explicit warning log for transient errors)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- resources_test.go: 6 tests covering US6.1 — CSI metadata filter
  (cinder.csi.openstack.org/cluster), transient HTTP 409 absorbed with
  verification triggered, HTTP 500 propagated, still-present → error,
  nothing-to-delete → no verification
- resources.go — DeleteSnapshots: aligned on FIP/LB/SG/Volume pattern
  (deleted=true before DELETE, explicit warning log for transient errors)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- resources_test.go: 5 tests covering US7.1 — successful deletion (204),
  already-deleted (404 → nil), forbidden (403 → graceful skip), HTTP 500
  propagated, no identity endpoint → CatalogError
- resources.go: no changes needed (implementation already correct)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- controller.go: add PurgeFunc/SleepFunc injectable fields; replace
  openstack.PurgeResources and time.Sleep calls with r.purge/r.sleep
  helpers (defaults preserved, no behavior change in production)
- openstackcluster_controller_test.go: 8 tests covering US8.1
  (finalizer add when absent, idempotent when present), US8.2 (cluster
  name from label vs metadata.name), US8.3 (finalizer removed after
  successful purge, cleanup skipped when finalizer absent), US8.4
  (retry annotation set on purge error, NotFound ignored when cluster
  deleted during retry)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- config_test.go: 10 tests covering US9.1 — DefaultVolumesFromEnv
  (default "delete", reads "keep", reads "delete"), RetryDelayFromEnv
  (default 60, reads 120, invalid value → fallback 60), reconciler
  volumes policy (global delete/keep, annotation overrides in both
  directions), retry delay applied to SleepFunc

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
US10.1 — Secure image
- Dockerfile: full rewrite from Python/kopf to Go multi-stage
  (golang:1.26 builder + gcr.io/distroless/static:nonroot, UID 65532)

US10.2 — Helm chart
- chart/values.yaml: add retryDefaultDelay (default 60 s)
- chart/templates/deployment.yaml: env CAPI_JANITOR_RETRY_DEFAULT_DELAY,
  livenessProbe (/healthz:8081) and readinessProbe (/readyz:8081)
- chart/templates/clusterrole.yaml: add "update" verb on openstackclusters
  (required by r.Update for finalizer management); remove stale kopf comments
- chart/tests/packaging_test.yaml: 11 helm-unittest tests covering US10.1
  and US10.2 (security context, injectable env vars, probes, RBAC)

US10.3 — Nix-based OCI build (no Flake) + SBOM
- nix/nixpkgs.nix: pin nixos-26.05 (Go 1.26 required)
- nix/default.nix: four attributes — manager (buildGoModule, CGO_ENABLED=0),
  amd64 image, arm64 image (cross-compiled via pkgsCross.aarch64-multiplatform),
  sbom (syft scan of the binary → CycloneDX JSON)
- .github/workflows/build-push-artifacts.yaml: replace docker-multiarch-build-push
  with nix-build + skopeo push + docker manifest for multi-arch support;
  SBOM generation uploaded as a CI artifact

Note: vendorHash = lib.fakeHash — update on first nix-build run.

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
…netes events) (TDD)

US11.1 — Prometheus metrics
- internal/controller/metrics.go: Metrics struct exposing
  capi_janitor_cleanups_total{result="success|failure"} CounterVec;
  NewMetrics(reg) registers it against any prometheus.Registerer
- OpenStackClusterReconciler.Metrics field (optional, injected in tests);
  SetupWithManager auto-registers against ctrlmetrics.Registry when nil
- incMetric("success"|"failure") called after each purge attempt

US11.2 — Kubernetes events
- OpenStackClusterReconciler.Recorder field (record.EventRecorder);
  SetupWithManager wires mgr.GetEventRecorderFor("capi-janitor") when nil
- Normal/CleanupSucceeded emitted on successful OpenStack cleanup
- Warning/CleanupFailed emitted on purge error (message carries the error)

Tests
- internal/controller/observability_test.go: 4 TDD tests using
  prometheus.NewRegistry() + prometheus/testutil.ToFloat64 for metric
  assertions, record.NewFakeRecorder for event assertions

go.mod: prometheus/client_golang promoted from indirect to direct dependency

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
…(TDD)

US12.1 — HTTP client timeout
- cloud.go: add http.Client.Timeout = 30s as a safety net for callers that
  do not set a context deadline; context cancellation remains the primary
  mechanism (all requests already use http.NewRequestWithContext)

US12.2 — Cinder legacy alias
- resources.go: cinderEndpoint() now tries "volumev3", "block-storage",
  then "volume" — adds compatibility with OpenStack installations older
  than Stein that advertise Cinder under the "volume" service type

Tests
- internal/openstack/robustness_test.go: 3 TDD tests
  - TestAuthenticate_ReturnsError_WhenContextAlreadyCancelled (US12.1)
  - TestCinderEndpoint_FallsBackToBlockStorage (regression)
  - TestCinderEndpoint_FallsBackToVolumeAlias (US12.2)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
@mgrzybek
mgrzybek requested a review from a team as a code owner July 8, 2026 11:44
mgrzybek and others added 11 commits July 8, 2026 14:02
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- packaging_test.yaml: remove `hasDocuments: count: 4` assertion —
  helm-unittest applies it per template file (not globally), so each
  file fails because it only renders 1 document. The snapshot test
  already validates the full set of rendered resources.
- snapshot_test.yaml.snap: regenerate to match current chart templates:
  - ClusterRole (snapshot 1): add "update" verb on openstackclusters
  - Deployment (snapshot 3): add CAPI_JANITOR_RETRY_DEFAULT_DELAY env var,
    livenessProbe (/healthz:8081) and readinessProbe (/readyz:8081)

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Reorder infrav1 imports per gofmt/goimports convention, set the real
vendorHash in nix/default.nix (was fakeHash), and update the README to
document `nix-build nix -A tests` and the macOS/Linux binary linkage
differences.

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Restore non-fatal handling of LB verification-list errors (Octavia is
known to be slower/less reliable, per PR #261), drop the unconditional
5s delay before the first deletion check, and stop the polling loop
promptly on context cancellation. Also fill in two test helpers that
had lost their IsAuthenticated() assertion, and document the polling
mechanism in ROADMAP.md.

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
- If a test file has been modified, it means that the behaviour is
  different. A integration test is then needed.
- If the helm chart has been modified, it means that the installation
  behaviour might change. We need to try to install the product.

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
…osystems

The image has been built via `nix-build nix -A image` since the CI rewrite;
remove the now-vestigial Dockerfile/.dockerignore and the Makefile
docker-build/docker-push/docker-buildx targets, and update every real
reference (README, ROADMAP, AGENTS.md, e2e image build/load) to the Nix
path. Also fix dependabot.yml, which tracked GitHub Actions and a
leftover Python `pip` ecosystem but nothing for go.mod: swap `pip` for
`gomod`.

Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
Signed-off-by: Mathieu Grzybek <mathieu@stackhpc.com>
@jihyun-huh

Copy link
Copy Markdown

hi, @mgrzybek @Banh-Canh this direction is really good to me, could i also add some commit on this PR?

@mgrzybek

Copy link
Copy Markdown
Contributor Author

hi, @mgrzybek @Banh-Canh this direction is really good to me, could i also add some commit on this PR?

Hi,

I think that we should merge this PR as fast as possible and then write new patches. I don’t like long-living PRs.

@mgrzybek
mgrzybek merged commit 764a1b0 into main Jul 13, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants