diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4951e33..72d2297 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,14 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone the code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: go.mod - name: Run linter - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: v1.63.4 + version: v2.14.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb95a6e..0b6e3db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,19 +16,23 @@ jobs: image: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 + env: + # GHCR links a multi-arch package to its repo via the index + # annotation; the Dockerfile LABEL only reaches the per-arch configs. + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: images: ${{ env.IMAGE }} tags: | @@ -38,11 +42,13 @@ jobs: type=semver,pattern={{major}}.{{minor}} - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} push: true cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc2e80d..9bb6941 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone the code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version-file: go.mod diff --git a/.golangci.yml b/.golangci.yml index 6b29746..a7246fb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,33 +1,15 @@ +version: "2" run: - timeout: 5m allow-parallel-runners: true - -issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) - exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" - linters: - - dupl - - lll linters: - disable-all: true + default: none enable: + - copyloopvar - dupl - errcheck - - copyloopvar - ginkgolinter - goconst - gocyclo - - gofmt - - goimports - - gosimple - govet - ineffassign - lll @@ -36,12 +18,34 @@ linters: - prealloc - revive - staticcheck - - typecheck - unconvert - unparam - unused - -linters-settings: - revive: + settings: + revive: + rules: + - name: comment-spacings + exclusions: + generated: lax rules: - - name: comment-spacings + - linters: + - lll + path: api/* + - linters: + - dupl + - lll + path: internal/* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile index 268373b..79ec3bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG TARGETARCH RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -trimpath -o /out/manager ./cmd FROM gcr.io/distroless/static:nonroot -LABEL org.opencontainers.image.source="https://github.com/lllamnyp/address-controller" +LABEL org.opencontainers.image.source="https://github.com/cozystack/address-controller" COPY --from=build /out/manager /manager USER 65532:65532 ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index c5401a8..551388b 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Image coordinates. TAG defaults to something unique per commit; override for # releases (e.g. make docker-push TAG=v0.1.0) or floating tags (TAG=main). -IMG_REPO ?= ghcr.io/lllamnyp/address-controller +IMG_REPO ?= ghcr.io/cozystack/address-controller TAG ?= $(shell git describe --tags --always --dirty) IMG ?= $(IMG_REPO):$(TAG) PLATFORMS ?= linux/amd64,linux/arm64 @@ -131,7 +131,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.17.1 ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.63.4 +GOLANGCI_LINT_VERSION ?= v2.14.0 .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. @@ -154,7 +154,7 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary diff --git a/PROJECT b/PROJECT index b947642..455572f 100644 --- a/PROJECT +++ b/PROJECT @@ -6,14 +6,14 @@ domain: sdn.cozystack.io layout: - go.kubebuilder.io/v4 projectName: address-controller -repo: github.com/lllamnyp/address-controller +repo: github.com/cozystack/address-controller resources: - api: crdVersion: v1 domain: sdn.cozystack.io group: local kind: IPAddressClass - path: github.com/lllamnyp/address-controller/api/v1alpha1 + path: github.com/cozystack/address-controller/api/v1alpha1 version: v1alpha1 - api: crdVersion: v1 @@ -21,7 +21,7 @@ resources: domain: sdn.cozystack.io group: local kind: IPAddress - path: github.com/lllamnyp/address-controller/api/v1alpha1 + path: github.com/cozystack/address-controller/api/v1alpha1 version: v1alpha1 - api: crdVersion: v1 @@ -30,6 +30,6 @@ resources: domain: sdn.cozystack.io group: local kind: IPAddressClaim - path: github.com/lllamnyp/address-controller/api/v1alpha1 + path: github.com/cozystack/address-controller/api/v1alpha1 version: v1alpha1 version: "3" diff --git a/README.md b/README.md index 675a0b7..b747c3b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ analogue), which live in separate projects. **The full contract — state machines, reconciliation algorithms, field ownership, and driver obligations — is specified in [docs/design.md](docs/design.md).** The reference driver implementing it is -[metallb-iad](https://github.com/lllamnyp/metallb-iad). +[metallb-iad](https://github.com/cozystack/metallb-iad). ## Resource model @@ -96,7 +96,7 @@ cluster. `helm uninstall` leaves the CRDs (and every `IPAddress` ledger entry) behind by design. The controller image is published as -`ghcr.io/lllamnyp/address-controller:main` (plus `main-` and semver tags) +`ghcr.io/cozystack/address-controller:main` (plus `main-` and semver tags) by the release workflow on every push to main. ## Development @@ -106,6 +106,6 @@ make manifests generate # regenerate CRDs, ClusterRole, and deepcopy after API make build # build the manager go test ./internal/controller/ make run # run the controller locally -make docker-build docker-push # publish ghcr.io/lllamnyp/address-controller: +make docker-build docker-push # publish ghcr.io/cozystack/address-controller: make helm-package # lint and package the chart into dist/ ``` diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 9ce34b5..9383fd4 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/ipaddress_types.go b/api/v1alpha1/ipaddress_types.go index 72b0b53..260aaf4 100644 --- a/api/v1alpha1/ipaddress_types.go +++ b/api/v1alpha1/ipaddress_types.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/ipaddressclaim_types.go b/api/v1alpha1/ipaddressclaim_types.go index 041a779..1b3a481 100644 --- a/api/v1alpha1/ipaddressclaim_types.go +++ b/api/v1alpha1/ipaddressclaim_types.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/ipaddressclass_types.go b/api/v1alpha1/ipaddressclass_types.go index 88fa871..555f2ca 100644 --- a/api/v1alpha1/ipaddressclass_types.go +++ b/api/v1alpha1/ipaddressclass_types.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/well_known.go b/api/v1alpha1/well_known.go index a2a2468..16124e6 100644 --- a/api/v1alpha1/well_known.go +++ b/api/v1alpha1/well_known.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index dfbbc60..76fd6c2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/chart/address-controller/Chart.yaml b/chart/address-controller/Chart.yaml index c721da9..fb6d953 100644 --- a/chart/address-controller/Chart.yaml +++ b/chart/address-controller/Chart.yaml @@ -10,6 +10,6 @@ keywords: - ipam - networking - cozystack -home: https://github.com/lllamnyp/address-controller +home: https://github.com/cozystack/address-controller sources: - - https://github.com/lllamnyp/address-controller + - https://github.com/cozystack/address-controller diff --git a/chart/address-controller/values.yaml b/chart/address-controller/values.yaml index d9ba718..08b9f72 100644 --- a/chart/address-controller/values.yaml +++ b/chart/address-controller/values.yaml @@ -1,5 +1,5 @@ # Container image for the core controller. -image: ghcr.io/lllamnyp/address-controller:main +image: ghcr.io/cozystack/address-controller:main imagePullPolicy: IfNotPresent imagePullSecrets: [] diff --git a/cmd/main.go b/cmd/main.go index 35e899f..df44207 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -38,8 +38,8 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" - localv1alpha1 "github.com/lllamnyp/address-controller/api/v1alpha1" - "github.com/lllamnyp/address-controller/internal/controller" + localv1alpha1 "github.com/cozystack/address-controller/api/v1alpha1" + "github.com/cozystack/address-controller/internal/controller" // +kubebuilder:scaffold:imports ) diff --git a/docs/design.md b/docs/design.md index 839509d..cb449f0 100644 --- a/docs/design.md +++ b/docs/design.md @@ -9,7 +9,7 @@ This document records the contract: the resource model, the two state machines the core drives, the exact reconciliation algorithms, and the obligations a per-class driver must meet. It is the reference against which -drivers (such as [metallb-iad](https://github.com/lllamnyp/metallb-iad)) +drivers (such as [metallb-iad](https://github.com/cozystack/metallb-iad)) are written. ## 1. Scope diff --git a/go.mod b/go.mod index ef5a2d2..62aaa52 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/lllamnyp/address-controller +module github.com/cozystack/address-controller -go 1.23.0 +go 1.26 godebug default=go1.23 diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index c47ddf8..aa99ee3 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/internal/controller/indexes.go b/internal/controller/indexes.go index 5ed8a8c..2d52f88 100644 --- a/internal/controller/indexes.go +++ b/internal/controller/indexes.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - localv1alpha1 "github.com/lllamnyp/address-controller/api/v1alpha1" + localv1alpha1 "github.com/cozystack/address-controller/api/v1alpha1" ) // IPAddressClaimRefIndex indexes IPAddress objects by the diff --git a/internal/controller/ipaddress_controller.go b/internal/controller/ipaddress_controller.go index 4584069..05fd45c 100644 --- a/internal/controller/ipaddress_controller.go +++ b/internal/controller/ipaddress_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - localv1alpha1 "github.com/lllamnyp/address-controller/api/v1alpha1" + localv1alpha1 "github.com/cozystack/address-controller/api/v1alpha1" ) // IPAddressReconciler owns the class-agnostic address lifecycle: deletion diff --git a/internal/controller/ipaddress_controller_test.go b/internal/controller/ipaddress_controller_test.go index 7780f61..ad5cece 100644 --- a/internal/controller/ipaddress_controller_test.go +++ b/internal/controller/ipaddress_controller_test.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - localv1alpha1 "github.com/lllamnyp/address-controller/api/v1alpha1" + localv1alpha1 "github.com/cozystack/address-controller/api/v1alpha1" ) func addressReconciler(c client.Client) *IPAddressReconciler { @@ -52,7 +52,7 @@ func unboundAddress(name, ip string) *localv1alpha1.IPAddress { return &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: ip, Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, @@ -73,9 +73,9 @@ func TestUnboundAddressBecomesAvailable(t *testing.T) { } func TestBoundAddressWithLiveClaimBecomesBound(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) addr := unboundAddress("ip-2", "203.0.113.2") - addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web", UID: "claim-uid-1"} + addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName, UID: testClaimUID} c := testClient(t, claim, addr) reconcileAddress(t, addressReconciler(c), "ip-2") @@ -85,9 +85,9 @@ func TestBoundAddressWithLiveClaimBecomesBound(t *testing.T) { } func TestPreBoundAddressWithoutUIDStaysUntouched(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) addr := unboundAddress("ip-3", "203.0.113.3") - addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web"} + addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName} c := testClient(t, claim, addr) reconcileAddress(t, addressReconciler(c), "ip-3") @@ -99,7 +99,7 @@ func TestPreBoundAddressWithoutUIDStaysUntouched(t *testing.T) { func TestOrphanedAddressRetainIsReleased(t *testing.T) { addr := unboundAddress("ip-4", "203.0.113.4") addr.Spec.ReclaimPolicy = localv1alpha1.ReclaimRetain - addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "gone", UID: "old-uid"} + addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: "gone", UID: "old-uid"} c := testClient(t, addr) reconcileAddress(t, addressReconciler(c), "ip-4") @@ -115,7 +115,7 @@ func TestOrphanedAddressRetainIsReleased(t *testing.T) { func TestOrphanedAddressDeletePolicyIsDeleted(t *testing.T) { addr := unboundAddress("ip-5", "203.0.113.5") addr.Spec.ReclaimPolicy = localv1alpha1.ReclaimDelete - addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "gone", UID: "old-uid"} + addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: "gone", UID: "old-uid"} c := testClient(t, addr) r := addressReconciler(c) reconcileAddress(t, r, "ip-5") @@ -130,9 +130,9 @@ func TestOrphanedAddressDeletePolicyIsDeleted(t *testing.T) { } func TestUIDMismatchTriggersReclaim(t *testing.T) { - claim := pendingClaim("public") // UID claim-uid-1 + claim := pendingClaim(testClassName) // UID claim-uid-1 addr := unboundAddress("ip-6", "203.0.113.6") - addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web", UID: "some-older-uid"} + addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName, UID: "some-older-uid"} c := testClient(t, claim, addr) reconcileAddress(t, addressReconciler(c), "ip-6") @@ -156,10 +156,10 @@ func TestStickyPhasesAreNotOverwritten(t *testing.T) { } func TestDeletionBlockedWhileBoundToLiveClaim(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) addr := unboundAddress("ip-7", "203.0.113.7") addr.Finalizers = []string{localv1alpha1.AddressProtectionFinalizer} - addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web", UID: "claim-uid-1"} + addr.Spec.ClaimRef = &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName, UID: testClaimUID} addr.Status.Phase = localv1alpha1.IPAddressBound c := testClient(t, claim, addr) if err := c.Delete(context.Background(), addr); err != nil { diff --git a/internal/controller/ipaddressclaim_controller.go b/internal/controller/ipaddressclaim_controller.go index 0429aec..d7975c4 100644 --- a/internal/controller/ipaddressclaim_controller.go +++ b/internal/controller/ipaddressclaim_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - localv1alpha1 "github.com/lllamnyp/address-controller/api/v1alpha1" + localv1alpha1 "github.com/cozystack/address-controller/api/v1alpha1" ) // IPAddressClaimReconciler owns the class-agnostic claim lifecycle: resolving @@ -371,7 +371,7 @@ func (r *IPAddressClaimReconciler) updateClaimStatus(ctx context.Context, claim previous := claim.Status.Phase sort.Slice(bound, func(i, j int) bool { return bound[i].Name < bound[j].Name }) - var reported []localv1alpha1.BoundAddress + reported := make([]localv1alpha1.BoundAddress, 0, len(bound)) for _, addr := range bound { reported = append(reported, localv1alpha1.BoundAddress{Name: addr.Name, Address: addr.Spec.Address}) } diff --git a/internal/controller/ipaddressclaim_controller_test.go b/internal/controller/ipaddressclaim_controller_test.go index 6527b29..ab4911b 100644 --- a/internal/controller/ipaddressclaim_controller_test.go +++ b/internal/controller/ipaddressclaim_controller_test.go @@ -1,5 +1,5 @@ /* -Copyright 2026 Timofei Larkin. +Copyright 2026 The Cozystack Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,7 +31,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - localv1alpha1 "github.com/lllamnyp/address-controller/api/v1alpha1" + localv1alpha1 "github.com/cozystack/address-controller/api/v1alpha1" +) + +const ( + testClassName = "public" + testProvisioner = "metallb.drivers.local.sdn.cozystack.io" + testNamespace = "tenant-a" + testClaimName = "web" + testClaimUID = "claim-uid-1" ) func testScheme(t *testing.T) *runtime.Scheme { @@ -70,20 +78,20 @@ func claimReconciler(c client.Client) *IPAddressClaimReconciler { } } -func reconcileClaim(t *testing.T, r *IPAddressClaimReconciler, namespace, name string) { +func reconcileClaim(t *testing.T, r *IPAddressClaimReconciler) { t.Helper() _, err := r.Reconcile(context.Background(), ctrl.Request{ - NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}, + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testClaimName}, }) if err != nil { t.Fatalf("reconcile: %v", err) } } -func getClaim(t *testing.T, c client.Client, namespace, name string) *localv1alpha1.IPAddressClaim { +func getClaim(t *testing.T, c client.Client) *localv1alpha1.IPAddressClaim { t.Helper() claim := &localv1alpha1.IPAddressClaim{} - if err := c.Get(context.Background(), types.NamespacedName{Namespace: namespace, Name: name}, claim); err != nil { + if err := c.Get(context.Background(), types.NamespacedName{Namespace: testNamespace, Name: testClaimName}, claim); err != nil { t.Fatalf("get claim: %v", err) } return claim @@ -100,17 +108,21 @@ func getAddress(t *testing.T, c client.Client, name string) *localv1alpha1.IPAdd func publicClass(annotations map[string]string) *localv1alpha1.IPAddressClass { return &localv1alpha1.IPAddressClass{ - ObjectMeta: metav1.ObjectMeta{Name: "public", Annotations: annotations}, + ObjectMeta: metav1.ObjectMeta{Name: testClassName, Annotations: annotations}, Spec: localv1alpha1.IPAddressClassSpec{ - Provisioner: "metallb.drivers.local.sdn.cozystack.io", + Provisioner: testProvisioner, ReclaimPolicy: localv1alpha1.ReclaimRetain, }, } } +func defaultClass() *localv1alpha1.IPAddressClass { + return publicClass(map[string]string{localv1alpha1.IsDefaultClassAnnotation: "true"}) +} + func pendingClaim(className string) *localv1alpha1.IPAddressClaim { return &localv1alpha1.IPAddressClaim{ - ObjectMeta: metav1.ObjectMeta{Name: "web", Namespace: "tenant-a", UID: "claim-uid-1"}, + ObjectMeta: metav1.ObjectMeta{Name: testClaimName, Namespace: testNamespace, UID: testClaimUID}, Spec: localv1alpha1.IPAddressClaimSpec{ ClassName: className, Family: localv1alpha1.FamilyIPv4, @@ -119,17 +131,17 @@ func pendingClaim(className string) *localv1alpha1.IPAddressClaim { } func TestClaimResolvesClassAndWaitsForProvisioner(t *testing.T) { - c := testClient(t, publicClass(nil), pendingClaim("public")) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + c := testClient(t, publicClass(nil), pendingClaim(testClassName)) + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") + claim := getClaim(t, c) if claim.Status.Phase != localv1alpha1.ClaimPending { t.Errorf("phase = %q, want Pending", claim.Status.Phase) } - if got := claim.Annotations[localv1alpha1.ProvisionerAnnotation]; got != "metallb.drivers.local.sdn.cozystack.io" { + if got := claim.Annotations[localv1alpha1.ProvisionerAnnotation]; got != testProvisioner { t.Errorf("provisioner annotation = %q", got) } - if claim.Status.ClassName != "public" { + if claim.Status.ClassName != testClassName { t.Errorf("status.className = %q", claim.Status.ClassName) } if !hasFinalizer(claim.Finalizers, localv1alpha1.ClaimProtectionFinalizer) { @@ -143,21 +155,21 @@ func TestClaimResolvesClassAndWaitsForProvisioner(t *testing.T) { func TestClaimUsesDefaultClass(t *testing.T) { c := testClient(t, - publicClass(map[string]string{localv1alpha1.IsDefaultClassAnnotation: "true"}), + defaultClass(), pendingClaim("")) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") - if claim.Status.ClassName != "public" { + claim := getClaim(t, c) + if claim.Status.ClassName != testClassName { t.Errorf("status.className = %q, want public (the default class)", claim.Status.ClassName) } } func TestClaimWithoutAnyDefaultClassStaysPending(t *testing.T) { c := testClient(t, publicClass(nil), pendingClaim("")) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") + claim := getClaim(t, c) cond := meta.FindStatusCondition(claim.Status.Conditions, localv1alpha1.ConditionClassResolved) if cond == nil || cond.Reason != localv1alpha1.ReasonNoDefaultClass { t.Errorf("ClassResolved condition = %+v, want reason NoDefaultClass", cond) @@ -165,15 +177,15 @@ func TestClaimWithoutAnyDefaultClassStaysPending(t *testing.T) { } func TestClaimWithMultipleDefaultClassesStaysPending(t *testing.T) { - second := publicClass(map[string]string{localv1alpha1.IsDefaultClassAnnotation: "true"}) + second := defaultClass() second.Name = "public-2" c := testClient(t, - publicClass(map[string]string{localv1alpha1.IsDefaultClassAnnotation: "true"}), + defaultClass(), second, pendingClaim("")) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") + claim := getClaim(t, c) cond := meta.FindStatusCondition(claim.Status.Conditions, localv1alpha1.ConditionClassResolved) if cond == nil || cond.Reason != localv1alpha1.ReasonMultipleDefaultClasses { t.Errorf("ClassResolved condition = %+v, want reason MultipleDefaultClasses", cond) @@ -184,24 +196,24 @@ func TestClaimAcceptsDriverPreBoundAddress(t *testing.T) { addr := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-203-0-113-7"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "203.0.113.7", // Driver pre-binds without a UID; the core controller completes it. - ClaimRef: &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web"}, + ClaimRef: &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName}, Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, } - c := testClient(t, publicClass(nil), pendingClaim("public"), addr) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + c := testClient(t, publicClass(nil), pendingClaim(testClassName), addr) + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") + claim := getClaim(t, c) if claim.Status.Phase != localv1alpha1.ClaimBound { t.Fatalf("phase = %q, want Bound", claim.Status.Phase) } if len(claim.Status.Addresses) != 1 || claim.Status.Addresses[0].Address != "203.0.113.7" { t.Errorf("status.addresses = %+v", claim.Status.Addresses) } - if got := getAddress(t, c, "ip-203-0-113-7").Spec.ClaimRef.UID; got != "claim-uid-1" { + if got := getAddress(t, c, "ip-203-0-113-7").Spec.ClaimRef.UID; got != testClaimUID { t.Errorf("claimRef.uid = %q, want completed to claim-uid-1", got) } } @@ -210,21 +222,21 @@ func TestClaimMatchesAvailableAddress(t *testing.T) { addr := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-203-0-113-8"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "203.0.113.8", Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, Status: localv1alpha1.IPAddressStatus{Phase: localv1alpha1.IPAddressAvailable}, } - c := testClient(t, publicClass(nil), pendingClaim("public"), addr) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + c := testClient(t, publicClass(nil), pendingClaim(testClassName), addr) + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") + claim := getClaim(t, c) if claim.Status.Phase != localv1alpha1.ClaimBound { t.Fatalf("phase = %q, want Bound", claim.Status.Phase) } ref := getAddress(t, c, "ip-203-0-113-8").Spec.ClaimRef - if ref == nil || ref.Namespace != "tenant-a" || ref.Name != "web" || ref.UID != "claim-uid-1" { + if ref == nil || ref.Namespace != testNamespace || ref.Name != testClaimName || ref.UID != testClaimUID { t.Errorf("claimRef = %+v", ref) } } @@ -242,27 +254,27 @@ func TestClaimIgnoresAvailableAddressOfWrongClassOrFamily(t *testing.T) { wrongFamily := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-wrong-family"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "2001:db8::9", Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, Status: localv1alpha1.IPAddressStatus{Phase: localv1alpha1.IPAddressAvailable}, } - c := testClient(t, publicClass(nil), pendingClaim("public"), wrongClass, wrongFamily) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + c := testClient(t, publicClass(nil), pendingClaim(testClassName), wrongClass, wrongFamily) + reconcileClaim(t, claimReconciler(c)) - if claim := getClaim(t, c, "tenant-a", "web"); claim.Status.Phase != localv1alpha1.ClaimPending { + if claim := getClaim(t, c); claim.Status.Phase != localv1alpha1.ClaimPending { t.Errorf("phase = %q, want Pending", claim.Status.Phase) } } func TestDualClaimNeedsBothFamilies(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) claim.Spec.Family = localv1alpha1.FamilyDual v4 := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-v4"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "203.0.113.10", Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, @@ -270,9 +282,9 @@ func TestDualClaimNeedsBothFamilies(t *testing.T) { } c := testClient(t, publicClass(nil), claim, v4) r := claimReconciler(c) - reconcileClaim(t, r, "tenant-a", "web") + reconcileClaim(t, r) - got := getClaim(t, c, "tenant-a", "web") + got := getClaim(t, c) if got.Status.Phase != localv1alpha1.ClaimPending { t.Fatalf("phase = %q, want Pending (v6 still missing)", got.Status.Phase) } @@ -283,7 +295,7 @@ func TestDualClaimNeedsBothFamilies(t *testing.T) { v6 := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-v6"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "2001:db8::10", Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, @@ -292,9 +304,9 @@ func TestDualClaimNeedsBothFamilies(t *testing.T) { if err := c.Create(context.Background(), v6); err != nil { t.Fatal(err) } - reconcileClaim(t, r, "tenant-a", "web") + reconcileClaim(t, r) - got = getClaim(t, c, "tenant-a", "web") + got = getClaim(t, c) if got.Status.Phase != localv1alpha1.ClaimBound { t.Fatalf("phase = %q, want Bound", got.Status.Phase) } @@ -304,15 +316,15 @@ func TestDualClaimNeedsBothFamilies(t *testing.T) { } func TestClaimDeletionRetainReleasesAddress(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) claim.Finalizers = []string{localv1alpha1.ClaimProtectionFinalizer} addr := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-retained"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "203.0.113.11", ReclaimPolicy: localv1alpha1.ReclaimRetain, - ClaimRef: &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web", UID: "claim-uid-1"}, + ClaimRef: &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName, UID: testClaimUID}, Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, Status: localv1alpha1.IPAddressStatus{Phase: localv1alpha1.IPAddressBound}, @@ -321,9 +333,9 @@ func TestClaimDeletionRetainReleasesAddress(t *testing.T) { if err := c.Delete(context.Background(), claim); err != nil { t.Fatal(err) } - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - err := c.Get(context.Background(), types.NamespacedName{Namespace: "tenant-a", Name: "web"}, &localv1alpha1.IPAddressClaim{}) + err := c.Get(context.Background(), types.NamespacedName{Namespace: testNamespace, Name: testClaimName}, &localv1alpha1.IPAddressClaim{}) if !apierrors.IsNotFound(err) { t.Errorf("claim still present after finalization: %v", err) } @@ -337,15 +349,15 @@ func TestClaimDeletionRetainReleasesAddress(t *testing.T) { } func TestClaimDeletionDeletePolicyDeletesAddress(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) claim.Finalizers = []string{localv1alpha1.ClaimProtectionFinalizer} addr := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-deleted"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "203.0.113.12", ReclaimPolicy: localv1alpha1.ReclaimDelete, - ClaimRef: &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web", UID: "claim-uid-1"}, + ClaimRef: &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName, UID: testClaimUID}, Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, Status: localv1alpha1.IPAddressStatus{Phase: localv1alpha1.IPAddressBound}, @@ -354,7 +366,7 @@ func TestClaimDeletionDeletePolicyDeletesAddress(t *testing.T) { if err := c.Delete(context.Background(), claim); err != nil { t.Fatal(err) } - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) err := c.Get(context.Background(), types.NamespacedName{Name: "ip-deleted"}, &localv1alpha1.IPAddress{}) if !apierrors.IsNotFound(err) { @@ -365,32 +377,32 @@ func TestClaimDeletionDeletePolicyDeletesAddress(t *testing.T) { func TestBoundClaimSurvivesClassDeletion(t *testing.T) { // A fully bound claim must reconcile without touching its class: // deleting the class breaks neither the binding nor status upkeep. - claim := pendingClaim("public") + claim := pendingClaim(testClassName) claim.Finalizers = []string{localv1alpha1.ClaimProtectionFinalizer} - claim.Annotations = map[string]string{localv1alpha1.ProvisionerAnnotation: "metallb.drivers.local.sdn.cozystack.io"} + claim.Annotations = map[string]string{localv1alpha1.ProvisionerAnnotation: testProvisioner} claim.Status = localv1alpha1.IPAddressClaimStatus{ Phase: localv1alpha1.ClaimBound, - ClassName: "public", + ClassName: testClassName, } addr := &localv1alpha1.IPAddress{ ObjectMeta: metav1.ObjectMeta{Name: "ip-203-0-113-14"}, Spec: localv1alpha1.IPAddressSpec{ - ClassName: "public", + ClassName: testClassName, Address: "203.0.113.14", - ClaimRef: &localv1alpha1.ClaimReference{Namespace: "tenant-a", Name: "web", UID: "claim-uid-1"}, + ClaimRef: &localv1alpha1.ClaimReference{Namespace: testNamespace, Name: testClaimName, UID: testClaimUID}, Source: localv1alpha1.IPAddressSource{FromClass: &localv1alpha1.FromClassSource{}}, }, Status: localv1alpha1.IPAddressStatus{Phase: localv1alpha1.IPAddressBound}, } // Note: no IPAddressClass object at all. c := testClient(t, claim, addr) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - got := getClaim(t, c, "tenant-a", "web") + got := getClaim(t, c) if got.Status.Phase != localv1alpha1.ClaimBound { t.Errorf("phase = %q, want Bound to survive class deletion", got.Status.Phase) } - if got.Status.ClassName != "public" { + if got.Status.ClassName != testClassName { t.Errorf("status.className = %q, want the sticky record untouched", got.Status.ClassName) } if len(got.Status.Addresses) != 1 || got.Status.Addresses[0].Address != "203.0.113.14" { @@ -402,9 +414,9 @@ func TestPendingClaimWithMissingClassRecordsClassNotFound(t *testing.T) { // An unbound claim naming a class that does not exist stays Pending // with the reason recorded; only provisioning is blocked. c := testClient(t, pendingClaim("nonexistent")) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - claim := getClaim(t, c, "tenant-a", "web") + claim := getClaim(t, c) if claim.Status.Phase != localv1alpha1.ClaimPending { t.Errorf("phase = %q, want Pending", claim.Status.Phase) } @@ -415,18 +427,18 @@ func TestPendingClaimWithMissingClassRecordsClassNotFound(t *testing.T) { } func TestBoundClaimGoesLostWhenAddressDisappears(t *testing.T) { - claim := pendingClaim("public") + claim := pendingClaim(testClassName) claim.Finalizers = []string{localv1alpha1.ClaimProtectionFinalizer} - claim.Annotations = map[string]string{localv1alpha1.ProvisionerAnnotation: "metallb.drivers.local.sdn.cozystack.io"} + claim.Annotations = map[string]string{localv1alpha1.ProvisionerAnnotation: testProvisioner} claim.Status = localv1alpha1.IPAddressClaimStatus{ Phase: localv1alpha1.ClaimBound, - ClassName: "public", + ClassName: testClassName, Addresses: []localv1alpha1.BoundAddress{{Name: "ip-gone", Address: "203.0.113.13"}}, } c := testClient(t, publicClass(nil), claim) - reconcileClaim(t, claimReconciler(c), "tenant-a", "web") + reconcileClaim(t, claimReconciler(c)) - got := getClaim(t, c, "tenant-a", "web") + got := getClaim(t, c) if got.Status.Phase != localv1alpha1.ClaimLost { t.Errorf("phase = %q, want Lost", got.Status.Phase) }