diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1f54db9..3a438ec 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -11,3 +11,15 @@ RUN curl -sSfLO https://github.com/operator-framework/operator-sdk/releases/down ARG GOLANGCI_VERSION=2.5.0 RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sudo sh -s -- v${GOLANGCI_VERSION} + +ARG KIND_VERSION=0.30.0 + +RUN curl -sSfLO https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-amd64 && \ + chmod +x kind-linux-amd64 && \ + sudo mv kind-linux-amd64 /usr/local/bin/kind + +ARG KUBECTL_VERSION=1.34.1 + +RUN curl -sSfLO https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \ + chmod +x kubectl && \ + sudo mv kubectl /usr/local/bin/kubectl diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..23d9901 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,62 @@ +name: "Build" + +on: + workflow_call: + inputs: + is-development: + description: "Whether the build is for development purposes or not" + required: false + default: true + type: boolean + is-latest: + description: "Whether the build is for the latest tag or not" + required: false + default: false + type: boolean + is-stable: + description: "Whether the build is for a stable release or not" + required: false + default: false + type: boolean + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + # NOTE: We fetch depth so that we can put the right `GIT` reference + fetch-depth: 0 + ref: ${{ github.ref }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to the registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Compute tags + id: tags + # If `is-development` then suffix tag with `-dev` + # If `is-latest` then add `latest` tag + # If not `is-stable` then suffix `-dev` to `latest + run: | + version="$(git describe --tags --always --dirty --match='v[0-9]*')${{ inputs.is-development && '-dev' || '' }}" + tags="ghcr.io/${{ github.repository }}:${version}" + if [ "${{ inputs.is-latest }}" = "true" ]; then + tags="$tags,ghcr.io/${{ github.repository }}:latest${{ ! inputs.is-stable && '-dev' || '' }}" + fi + echo "tags=$tags" >> $GITHUB_OUTPUT + echo "version=$version" >> $GITHUB_OUTPUT + - name: Build image + uses: docker/build-push-action@v6 + with: + context: . + build-args: + VERSION=${{ steps.tags.outputs.version }} + push: true + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha,scope=crl-operator + cache-to: type=gha,mode=max,scope=crl-operator diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 86e3845..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Lint - -on: - push: - pull_request: - -jobs: - lint: - name: Run on Ubuntu - runs-on: ubuntu-latest - steps: - - name: Clone the code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Run linter - uses: golangci/golangci-lint-action@v8 - with: - version: v2.1.0 diff --git a/.github/workflows/post-merge.yaml b/.github/workflows/post-merge.yaml new file mode 100644 index 0000000..9aa0965 --- /dev/null +++ b/.github/workflows/post-merge.yaml @@ -0,0 +1,15 @@ +name: "Post Merge" + +on: + push: + branches: + - main + +jobs: + build: + uses: ./.github/workflows/build.yaml + secrets: inherit + with: + is-development: false + is-latest: true + is-stable: false diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml new file mode 100644 index 0000000..7ac8051 --- /dev/null +++ b/.github/workflows/pre-merge.yaml @@ -0,0 +1,55 @@ +name: "Pre Merge" + +on: + workflow_dispatch: + + pull_request: + branches: + - main + +jobs: + build: + uses: ./.github/workflows/build.yaml + secrets: inherit + with: + is-development: true + is-latest: false + is-stable: false + + generate: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Run code generation + # We run code generation to ensure that the generated code is up to date + run: | + go generate ./... && + make generate manifests && + git diff --quiet + + lint: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Run linters + uses: golangci/golangci-lint-action@v8 + with: + version: v2.5.0 + + test: + uses: ./.github/workflows/test.yml + secrets: inherit + + test-e2e: + uses: ./.github/workflows/test-e2e.yml + secrets: inherit diff --git a/.github/workflows/promote.yaml b/.github/workflows/promote.yaml new file mode 100644 index 0000000..608306b --- /dev/null +++ b/.github/workflows/promote.yaml @@ -0,0 +1,39 @@ +name: Promote +run-name: "Promote ${{ github.ref_name }}" + +on: + push: + tags: + - "v*" + +jobs: + build: + uses: ./.github/workflows/build.yaml + secrets: inherit + with: + is-development: false + is-latest: true + is-stable: ${{ ! contains(github.ref_name, '-') }} + + create-release: + needs: build + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # NOTE: We explicitly set the refs otherwise the tag + # annotations content is not fetched + # See: https://github.com/actions/checkout/issues/882 + ref: ${{ github.ref }} + - uses: softprops/action-gh-release@v2 + with: + name: CRL Operator ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + generate_release_notes: true + # We consider pre-releases if the tag contains a hyphen + # e.g. v1.2.3-alpha.0 + prerelease: ${{ contains(github.ref_name, '-') }} + draft: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..628a4e4 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,83 @@ +name: "Release" +run-name: Release new ${{ inputs.version-type }} from ${{ github.ref_name }} + +on: + workflow_dispatch: + inputs: + version-type: + description: "Version type" + required: true + type: choice + default: "alpha" + options: + - "alpha" + - "beta" + - "GA" + version-scope: + description: "Version scope" + required: true + type: choice + default: "patch" + options: + - "patch" + - "minor" + - "major" + +jobs: + prepare-version: + runs-on: ubuntu-24.04 + if: github.ref_name == 'main' + steps: + - uses: actions/create-github-app-token@v2 + id: app-token + # NOTE: This is needed otherwise it's the same user that create the tag + # than the one triggering the workflow on push tag which does not work + with: + app-id: ${{ vars.ACTIONS_APP_ID }} + private-key: ${{ secrets.ACTIONS_APP_PRIVATE_KEY }} + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + - name: Install semver tool + run: | + curl --fail -LO https://raw.githubusercontent.com/fsaintjacques/semver-tool/3.4.0/src/semver + chmod +x ./semver + - name: Compose release tag + run: | + last_ga_tag="$(git tag --sort=taggerdate --list "v*" | grep -v '\-' | tail -n 1)" + if [[ -z "$last_ga_tag" ]]; then + last_ga_tag="0.0.0" + fi + + new_version="$(./semver bump "${{ inputs.version-scope }}" "$last_ga_tag")" + + if [[ "${{ inputs.version-type }}" == "alpha" ]] || [[ "${{ inputs.version-type }}" == "beta" ]]; then + last_pre_tag="$(git tag --sort=taggerdate --list "v$new_version-${{ inputs.version-type }}.*" | tail -n 1)" + if [[ -z "$last_pre_tag" ]]; then + new_version="$new_version-${{ inputs.version-type }}.1" + else + new_version="$(./semver bump prerel "$last_pre_tag")" + fi + fi + + if [[ "${new_version:0:1}" != "v" ]]; then + new_version="v$new_version" + fi + + echo "New version: $new_version" + echo "RELEASE_TAG=$new_version" >> "$GITHUB_ENV" + - name: Validate ${{ env.RELEASE_TAG }} tag + run: ./semver validate ${{ env.RELEASE_TAG }} + + - name: Create and push `${{ env.RELEASE_TAG }}` tag + run: | + git fsck + git gc + + git config --global user.email ${{ github.actor }}@scality.com + git config --global user.name ${{ github.actor }} + + git tag -a "${{ env.RELEASE_TAG }}" -m "CRL Operator ${{ env.RELEASE_TAG }}" + git push origin "${{ env.RELEASE_TAG }}" diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 68fd1ed..46215b4 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,25 +1,26 @@ name: E2E Tests on: - push: - pull_request: + workflow_call: jobs: test-e2e: - name: Run on Ubuntu - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + # Disable e2e tests since there is none for now + if: false steps: - - name: Clone the code + - name: Checkout uses: actions/checkout@v4 - - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - - name: Install the latest version of kind + - name: Install kind + env: + KIND_VERSION: v0.30.0 run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64 chmod +x ./kind sudo mv ./kind /usr/local/bin/kind diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc2e80d..9cf76a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,17 +1,14 @@ name: Tests on: - push: - pull_request: + workflow_call: jobs: test: - name: Run on Ubuntu - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - name: Clone the code + - name: Checkout uses: actions/checkout@v4 - - name: Setup Go uses: actions/setup-go@v5 with: diff --git a/.gitignore b/.gitignore index ccabe5e..edf4b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ Dockerfile.cross coverage.* *.coverprofile profile.cov +testdata # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/Makefile b/Makefile index 5971f2e..3589f71 100644 --- a/Makefile +++ b/Makefile @@ -109,9 +109,19 @@ vet: ## Run go vet against code. go vet ./... .PHONY: test -test: manifests generate fmt vet setup-envtest ## Run tests. +test: manifests generate fmt vet setup-envtest download-extra-crds ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out +CERT_MANAGER_VERSION := v1.19.1 +testdata/crds/cert-manager-crds.yaml: + @mkdir -p $(@D) + curl -sSLo $@ \ + https://github.com/cert-manager/cert-manager/releases/download/$(CERT_MANAGER_VERSION)/cert-manager.crds.yaml + + +.PHONY: download-extra-crds +download-extra-crds: testdata/crds/cert-manager-crds.yaml + # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # CertManager is installed by default; skip with: diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index 0cb90df..fa26a65 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -717,6 +717,9 @@ func (r *ManagedCRLReconciler) crlNeedRenewal(currentCRL *x509.RevocationList, r } // Check if the CRL contains all revoked certificates + if len(revokedList) != len(currentCRL.RevokedCertificateEntries) { + return true + } // NOTE: We manage the full list so we expect a match in the same order for i, revoked := range revokedList { if i >= len(currentCRL.RevokedCertificateEntries) { diff --git a/internal/controller/managedcrl_controller_test.go b/internal/controller/managedcrl_controller_test.go index 3aeaa6e..9b98a3e 100644 --- a/internal/controller/managedcrl_controller_test.go +++ b/internal/controller/managedcrl_controller_test.go @@ -23,7 +23,6 @@ import ( . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -66,6 +65,7 @@ var _ = Describe("ManagedCRL Controller", func() { By("Cleanup the specific resource instance ManagedCRL") Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) }) + /* This is tested in Integration tests It("should successfully reconcile the resource", func() { By("Reconciling the created resource") controllerReconciler := &ManagedCRLReconciler{ @@ -80,5 +80,6 @@ var _ = Describe("ManagedCRL Controller", func() { // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. // Example: If you expect a certain status condition after reconciliation, verify it here. }) + */ }) }) diff --git a/test/integration/managedcrl_controller_test.go b/test/integration/managedcrl_controller_test.go new file mode 100644 index 0000000..29039a2 --- /dev/null +++ b/test/integration/managedcrl_controller_test.go @@ -0,0 +1,616 @@ +/* +Copyright 2025 Scality. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "context" + "crypto/x509" + "fmt" + "math/big" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1" +) + +type mcrlTestCase struct { + name string + spec crloperatorv1alpha1.ManagedCRLSpec + shouldError bool + shouldExposePod bool + shouldExposeIngress bool + shouldConfigureIssuer bool +} + +var ( + testCases = []mcrlTestCase{ + { + name: "secret-only", + spec: crloperatorv1alpha1.ManagedCRLSpec{}, + }, { + name: "exposed-only", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + }, + }, + shouldExposePod: true, + }, { + name: "exposed-with-custom-im", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Image: &crloperatorv1alpha1.ImageSpec{Repository: ptr.To("custom/repo"), Tag: ptr.To("v1.2.3")}, + Internal: ptr.To(false), + }, + }, + shouldExposePod: true, + }, { + name: "exposed-only-explicit-ingress-false", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + Ingress: &crloperatorv1alpha1.IngressSpec{ + Enabled: ptr.To(false), + }, + }, + }, + shouldExposePod: true, + }, { + name: "ingress-only-hostname", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + Ingress: &crloperatorv1alpha1.IngressSpec{ + Hostname: ptr.To("test.local"), + }, + }, + }, + shouldExposePod: true, + shouldExposeIngress: true, + shouldConfigureIssuer: true, + }, { + name: "ingress-only-ip", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + Ingress: &crloperatorv1alpha1.IngressSpec{ + IPAddresses: []crloperatorv1alpha1.IPAddress{ + "10.11.12.13", + "20.21.22.23", + }, + }, + }, + }, + shouldExposePod: true, + shouldExposeIngress: true, + shouldConfigureIssuer: true, + }, { + name: "ingress-only-both", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + Ingress: &crloperatorv1alpha1.IngressSpec{ + Hostname: ptr.To("test.local"), + IPAddresses: []crloperatorv1alpha1.IPAddress{ + "10.11.12.13", + "20.21.22.23", + }, + }, + }, + }, + shouldExposePod: true, + shouldExposeIngress: true, + shouldConfigureIssuer: true, + }, { + name: "exposed-with-internal", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(true), + }, + }, + shouldExposePod: true, + shouldConfigureIssuer: true, + }, { + name: "ingress-not-managed", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + Ingress: &crloperatorv1alpha1.IngressSpec{ + Managed: ptr.To(false), + Hostname: ptr.To("test.local"), + IPAddresses: []crloperatorv1alpha1.IPAddress{ + "10.11.12.13", + "20.21.22.23", + }, + }, + }, + }, + shouldExposePod: true, + shouldConfigureIssuer: true, + }, { + name: "all-in-one", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Ingress: &crloperatorv1alpha1.IngressSpec{ + Hostname: ptr.To("test.local"), + IPAddresses: []crloperatorv1alpha1.IPAddress{ + "10.11.12.13", + "20.21.22.23", + }, + }, + }, + }, + shouldExposePod: true, + shouldExposeIngress: true, + shouldConfigureIssuer: true, + }, + } +) + +func toTableEntry(tcs []mcrlTestCase) []TableEntry { + entries := []TableEntry{} + for _, tc := range tcs { + // Always add one entry for Issuer and one for ClusterIssuer + name := tc.name + tc.spec.IssuerRef.Name = "test-issuer" + + tc.name = fmt.Sprintf("%s-issuer", name) + tc.spec.IssuerRef.Kind = "Issuer" + entries = append(entries, Entry(fmt.Sprintf("ManagedCRL %s", tc.name), tc)) + + tc.name = fmt.Sprintf("%s-clusterissuer", name) + tc.spec.IssuerRef.Kind = "ClusterIssuer" + entries = append(entries, Entry(fmt.Sprintf("ManagedCRL %s", tc.name), tc)) + } + return entries +} + +var _ = Describe("ManagedCRL Controller", func() { + Context("When reconciling a resource", func() { + var testNamespace string + ctx := context.Background() + + BeforeEach(func() { + testNamespace = fmt.Sprintf("test-mcrl-%d", time.Now().UnixNano()) + Expect(k8sClient.Create( + ctx, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + }, + )).To(Succeed()) + + By("creating issuers required for the tests") + Expect(k8sClient.Create( + ctx, + &cmv1.ClusterIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-issuer", + }, + Spec: cmv1.IssuerSpec{ + IssuerConfig: cmv1.IssuerConfig{ + CA: &cmv1.CAIssuer{ + SecretName: "ca-key-pair", + }, + }, + }, + }, + )).To(Succeed()) + Expect(k8sClient.Create( + ctx, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-key-pair", + Namespace: testNamespace, + }, + Data: map[string][]byte{ + "tls.key": []byte(caKeyPem), + "tls.crt": []byte(caCrtPem), + }, + Type: corev1.SecretTypeTLS, + }, + )).To(Succeed()) + Expect(k8sClient.Create( + ctx, + &cmv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-issuer", + Namespace: testNamespace, + }, + Spec: cmv1.IssuerSpec{ + IssuerConfig: cmv1.IssuerConfig{ + CA: &cmv1.CAIssuer{ + SecretName: "ca-key-pair", + }, + }, + }, + }, + )).To(Succeed()) + }) + + AfterEach(func() { + Expect(k8sClient.Delete(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) + + Expect(k8sClient.Delete(ctx, &cmv1.ClusterIssuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-issuer", + }, + })).To(Succeed()) + }) + + DescribeTableSubtree("should reconcile various ManagedCRL resources as expected", func(tc mcrlTestCase) { + It("should successfully reconcile resource", func() { + typeNamespacedName := types.NamespacedName{ + Name: tc.name, + Namespace: testNamespace, + } + managedcrl := &crloperatorv1alpha1.ManagedCRL{ + ObjectMeta: metav1.ObjectMeta{ + Name: typeNamespacedName.Name, + Namespace: typeNamespacedName.Namespace, + }, + Spec: tc.spec, + } + + By("creating the ManagedCRL") + err := k8sClient.Create(ctx, managedcrl) + if tc.shouldError { + Expect(err).To(HaveOccurred()) + return + } + Expect(err).ToNot(HaveOccurred()) + checkAllReady(typeNamespacedName, tc, true) + + By("adding a revoked certificate to the CRL and checking the update is reflected") + retrieved := &crloperatorv1alpha1.ManagedCRL{} + Expect(k8sClient.Get(ctx, typeNamespacedName, retrieved)).To(Succeed()) + retrieved.Spec.Revocations = []crloperatorv1alpha1.RevocationSpec{ + { + SerialNumber: "123456789", + ReasonCode: ptr.To(2), + }, + } + Expect(k8sClient.Update(ctx, retrieved)).To(Succeed()) + checkAllReady(typeNamespacedName, tc, false) + + By("changing the reason code of a revoked certificate") + Expect(k8sClient.Get(ctx, typeNamespacedName, retrieved)).To(Succeed()) + retrieved.Spec.Revocations = []crloperatorv1alpha1.RevocationSpec{ + { + SerialNumber: "123456789", + ReasonCode: ptr.To(1), + }, + } + Expect(k8sClient.Update(ctx, retrieved)).To(Succeed()) + checkAllReady(typeNamespacedName, tc, false) + + By("removing all revoked certificates from the CRL") + Expect(k8sClient.Get(ctx, typeNamespacedName, retrieved)).To(Succeed()) + retrieved.Spec.Revocations = nil + Expect(k8sClient.Update(ctx, retrieved)).To(Succeed()) + checkAllReady(typeNamespacedName, tc, false) + + By("deleting the ManagedCRL") + Expect(k8sClient.Delete(ctx, retrieved)).To(Succeed()) + Eventually(func() bool { + return errors.IsNotFound( + k8sClient.Get(ctx, typeNamespacedName, &crloperatorv1alpha1.ManagedCRL{}), + ) + }, 10*time.Second, time.Second).Should(BeTrue()) + }) + }, toTableEntry(testCases)) + }) +}) + +func checkAllReady(mcrlRef types.NamespacedName, tc mcrlTestCase, podShouldRestart bool) { + By("checking the ManagedCRL becomes Secret properly setup") + checkSecret(mcrlRef) + + if tc.shouldExposePod { + By("checking the ManagedCRL becomes PodExposed properly setup") + checkExposePod(mcrlRef, podShouldRestart) + } else { + By("checking no PodExposed status is set") + retrieved := &crloperatorv1alpha1.ManagedCRL{} + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + Expect(retrieved.Status.PodExposed).To(BeNil()) + } + if tc.shouldExposeIngress { + By("checking the ManagedCRL becomes IngressExposed properly setup") + checkIngress(mcrlRef) + } else { + By("checking no IngressExposed status is set") + retrieved := &crloperatorv1alpha1.ManagedCRL{} + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + Expect(retrieved.Status.IngressExposed).To(BeNil()) + } + if tc.shouldConfigureIssuer { + By("checking the ManagedCRL becomes IssuerConfigured properly setup") + checkIssuerConfigured(mcrlRef) + } else { + By("checking no IssuerConfigured status is set") + retrieved := &crloperatorv1alpha1.ManagedCRL{} + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + Expect(retrieved.Status.IssuerConfigured).To(BeNil()) + } +} + +// Check if the given Secret matches the expected values from the ManagedCRL +func checkSecret(mcrlRef types.NamespacedName) { + retrieved := &crloperatorv1alpha1.ManagedCRL{} + + // Wait until the ManagedCRL is SecretReady + Eventually(func() bool { + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + for _, cond := range retrieved.Status.Conditions { + if cond.Type == "SecretReady" { + return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == retrieved.Generation + } + } + return false + }, 10*time.Second, time.Second).Should(BeTrue()) + retrieved.WithDefaults() + + expectedSecretNs := mcrlRef.Namespace + if retrieved.Spec.IssuerRef.Kind == "ClusterIssuer" { + expectedSecretNs = certManagerNamespace + } + Expect(retrieved.Status.SecretReady).To(PointTo(BeTrue())) + Expect(retrieved.Status.ObservedCASecretRef).To(PointTo(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("ca-key-pair"), + "Namespace": Equal(expectedSecretNs), + }))) + Expect(retrieved.Status.ObservedCASecretVersion).ToNot(BeEmpty()) + + // Check secret content + expectedRevokedCerts, err := retrieved.Spec.GetRevokedListEntries() + Expect(err).ToNot(HaveOccurred()) + createdSecret := retrieved.GetSecret() + Expect(k8sClient.Get( + ctx, + client.ObjectKeyFromObject(createdSecret), + createdSecret, + )).To(Succeed()) + Expect(createdSecret.Data).To(HaveKey("ca.crl")) + Expect(createdSecret.OwnerReferences).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "APIVersion": Equal(crloperatorv1alpha1.GroupVersion.String()), + "Kind": Equal("ManagedCRL"), + "Name": Equal(retrieved.Name), + "UID": Equal(retrieved.UID), + }))) + + // Check CRL content + crl, err := x509.ParseRevocationList(createdSecret.Data["ca.crl"]) + Expect(err).ToNot(HaveOccurred()) + Expect(crl).ToNot(BeNil()) + Expect(crl.Number).To(Equal(big.NewInt(retrieved.Status.CRLNumber))) + Expect(crl.CheckSignatureFrom(caCert)).ToNot(HaveOccurred()) + if len(expectedRevokedCerts) == 0 { + Expect(crl.RevokedCertificateEntries).To(BeEmpty()) + } else { + for i, revokedCert := range expectedRevokedCerts { + Expect(crl.RevokedCertificateEntries[i].SerialNumber).To(Equal(revokedCert.SerialNumber)) + Expect(crl.RevokedCertificateEntries[i].ReasonCode).To(Equal(revokedCert.ReasonCode)) + } + } +} + +// checkExposePod is a helper to check if the PodExposed condition is set as expected +func checkExposePod(mcrlRef types.NamespacedName, shouldRestart bool) { + retrieved := &crloperatorv1alpha1.ManagedCRL{} + + if shouldRestart { + By("checking the deployment is restarted when already present") + // Wait until the ManagedCRL is PodExposed False since the deployment is not ready yet + Eventually(func() bool { + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + for _, cond := range retrieved.Status.Conditions { + if cond.Type == "PodExposed" { + return cond.Status == metav1.ConditionFalse && + cond.ObservedGeneration == retrieved.Generation && + cond.Reason == "ServerPodNotReady" + } + } + return false + }, 10*time.Second, time.Second).Should(BeTrue()) + retrieved.WithDefaults() + Expect(retrieved.Status.PodExposed).To(PointTo(BeFalse())) + + // Check the deployment + createdDeployment := retrieved.GetDeployment() + Expect(k8sClient.Get( + ctx, + client.ObjectKeyFromObject(createdDeployment), + createdDeployment, + )).To(Succeed()) + Expect(createdDeployment.Spec.Replicas).To(PointTo(BeEquivalentTo(2))) + Expect(createdDeployment.OwnerReferences).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "APIVersion": Equal(crloperatorv1alpha1.GroupVersion.String()), + "Kind": Equal("ManagedCRL"), + "Name": Equal(retrieved.Name), + "UID": Equal(retrieved.UID), + }))) + Expect(createdDeployment.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(createdDeployment.Spec.Template.Spec.Containers[0].Image).To(Equal(retrieved.Spec.Expose.Image.GetImage())) + + // Set the deployment as ready + createdDeployment.Status.Replicas = 2 + createdDeployment.Status.ReadyReplicas = 2 + Expect(k8sClient.Status().Update( + ctx, + createdDeployment, + )).To(Succeed()) + } + + // Wait until the ManagedCRL is PodExposed + Eventually(func() bool { + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + for _, cond := range retrieved.Status.Conditions { + if cond.Type == "PodExposed" { + return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == retrieved.Generation + } + } + return false + }, 10*time.Second, time.Second).Should(BeTrue()) + retrieved.WithDefaults() + + Expect(retrieved.Status.PodExposed).To(PointTo(BeTrue())) + + // Check ConfigMap existence + createdConfigMap := retrieved.GetConfigMap() + Expect(k8sClient.Get( + ctx, + client.ObjectKeyFromObject(createdConfigMap), + createdConfigMap, + )).To(Succeed()) + Expect(createdConfigMap.Data).To(HaveKey("default.conf")) + Expect(createdConfigMap.OwnerReferences).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "APIVersion": Equal(crloperatorv1alpha1.GroupVersion.String()), + "Kind": Equal("ManagedCRL"), + "Name": Equal(retrieved.Name), + "UID": Equal(retrieved.UID), + }))) + + // Check Service existence + createdService := retrieved.GetService() + Expect(k8sClient.Get( + ctx, + client.ObjectKeyFromObject(createdService), + createdService, + )).To(Succeed()) + Expect(createdService.OwnerReferences).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "APIVersion": Equal(crloperatorv1alpha1.GroupVersion.String()), + "Kind": Equal("ManagedCRL"), + "Name": Equal(retrieved.Name), + "UID": Equal(retrieved.UID), + }))) +} + +// checkIngress is a helper to check if the IngressExposed condition is set as expected +func checkIngress(mcrlRef types.NamespacedName) { + retrieved := &crloperatorv1alpha1.ManagedCRL{} + + // Wait until the ManagedCRL is IngressExposed + Eventually(func() bool { + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + for _, cond := range retrieved.Status.Conditions { + if cond.Type == "IngressExposed" { + return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == retrieved.Generation + } + } + return false + }, 10*time.Second, time.Second).Should(BeTrue()) + retrieved.WithDefaults() + + Expect(retrieved.Status.IngressExposed).To(PointTo(BeTrue())) + + // Check Ingress existence + createdIngress := retrieved.GetIngress() + Expect(k8sClient.Get( + ctx, + client.ObjectKeyFromObject(createdIngress), + createdIngress, + )).To(Succeed()) + Expect(createdIngress.OwnerReferences).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "APIVersion": Equal(crloperatorv1alpha1.GroupVersion.String()), + "Kind": Equal("ManagedCRL"), + "Name": Equal(retrieved.Name), + "UID": Equal(retrieved.UID), + }))) + Expect(createdIngress.Spec.IngressClassName).To(Equal(retrieved.Spec.Expose.Ingress.ClassName)) + index := 0 + if retrieved.Spec.Expose.Ingress.Hostname != nil { + Expect(len(createdIngress.Spec.Rules)).To(BeNumerically(">", index)) + Expect(createdIngress.Spec.Rules[index].Host).To(Equal(*retrieved.Spec.Expose.Ingress.Hostname)) + index += 1 + } + if len(retrieved.Spec.Expose.Ingress.IPAddresses) > 0 { + Expect(len(createdIngress.Spec.Rules)).To(BeNumerically(">", index)) + Expect(createdIngress.Spec.Rules[index].Host).To(BeEmpty()) + } +} + +// checkIssuerConfigured is a helper to check if the IssuerConfigured condition is set as expected +func checkIssuerConfigured(mcrlRef types.NamespacedName) { + retrieved := &crloperatorv1alpha1.ManagedCRL{} + + // Wait until the ManagedCRL is IssuerConfigured + Eventually(func() bool { + Expect(k8sClient.Get(ctx, mcrlRef, retrieved)).To(Succeed()) + for _, cond := range retrieved.Status.Conditions { + if cond.Type == "IssuerConfigured" { + return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == retrieved.Generation + } + } + return false + }, 10*time.Second, time.Second).Should(BeTrue()) + retrieved.WithDefaults() + + Expect(retrieved.Status.IssuerConfigured).To(PointTo(BeTrue())) + + // Retrieve the Issuer/ClusterIssuer and check it + switch retrieved.Spec.IssuerRef.Kind { + case "Issuer": + issuer := &cmv1.Issuer{} + Expect(k8sClient.Get( + ctx, + types.NamespacedName{ + Name: retrieved.Spec.IssuerRef.Name, + Namespace: mcrlRef.Namespace, + }, + issuer, + )).To(Succeed()) + Expect(issuer.Spec.CA.CRLDistributionPoints).To(Equal(retrieved.GetCRLDistributionPoint())) + case "ClusterIssuer": + clusterIssuer := &cmv1.ClusterIssuer{} + Expect(k8sClient.Get( + ctx, + types.NamespacedName{ + Name: retrieved.Spec.IssuerRef.Name, + }, + clusterIssuer, + )).To(Succeed()) + Expect(clusterIssuer.Spec.CA.CRLDistributionPoints).To(Equal(retrieved.GetCRLDistributionPoint())) + default: + Fail("unexpected IssuerRef.Kind") + } +} diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go new file mode 100644 index 0000000..5506df2 --- /dev/null +++ b/test/integration/suite_test.go @@ -0,0 +1,267 @@ +/* +Copyright 2025 Scality. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1" + "github.com/scality/crl-operator/internal/controller" + // +kubebuilder:scaffold:imports +) + +const ( + certManagerNamespace = "cert-manager" + + caKeyPem = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrc62n/+wkhl1e +s20//KOB8Ce1ZiTUOnmJ40YiH05bgNyN4myjYxLASq+pmp4eVUqgEOUWU1ESPzwv +M+ZkaaHv77xXWh/pQgdaaTmSJsZf6PQAitNR1/zR/GjZfgGAHXxF3+d2t8MYwG9i +3q+Vh0ERKTaK89PYhqvoGiyxpDZT06QxLTHuR0RP12FfPvFREL0B5qp+7CAyoWbG +kDAWEl2HSVTOnMeS1zwJdgXElKxh0c6ABuOU6QIInQ+eWzz8HqusweruseG8AI9x +j6sL+etQH8JzQ0ewl10v5bA5mwMIa8aILBCKrAsA6YCtIThu/dh/8lCaGfq+Xsbh +4gxsYJy1AgMBAAECggEAB+H9LCS28Y+gmlqLMtka7BNObWeBWPqenOmEpMv7WSH/ +F0zqQXGBgAgkXxq7RgTA4MQgwZqEtzSOFVOAYRcpjrO0h/BJAvtPWueh8dIeO8mk +8k4aXNXJNh8HSRH4irCQW/wT6HWWyZFgwv3JuokUmO/j+0EuTnMT/fYPc6lrpVtE +j2LSfw6JCuLksYxui+J7zI94vROS8HWY/ZVyktmL81Q47y9yC9uNI/bpXzFrLWLA +qAfTE99hZfO2bBWe0AWMcadpDWap9wvk3TJSa6L8xOr8gRp1FbQzM1TFy5K+hFnm +NZEor9Z5FSQ7nYni8jBK+ywFaS9RQfYOLyNHyMmaIQKBgQDf8k6ItA9N0rFoxf6G +BPoqSlVDOGyxmkFmN6xHygeFpKho8A1yHZdeIedtuUdENEpbnlm6U3ulwrRr687X +V03nn7sos3HG1TrTnh4wi/Zxt/+TcR5qUmWHa2bYuFtSWlG7RV7fX+YBz8yDDJkV +43Fr5vV60tzHuEgrEh/aNFDUUQKBgQDD/eYwpoa9tq+oKQIbUpIpKgPZGqjysRTJ +weZtvqEvAjR9TRqJNzS1dkWgdeOOpzoL1CtYt7IZSCaZt7o2sOuEYlbNYJYYsFMh +MLMr28dYWrv3GMZqShsLoT06OlTvz89Q7m7OyH00B89QpyFVh3bbr7bglD5PBd0M +vGP+7OjdJQKBgQCCMCftmtemw0x1f1zW5n/UJABrIps1qFpKpSTXWyCCVdW9o4f9 +hixgAc+7XtGKWee8WVMKWcvw8j7W2nAVieB1Pcuc/qyvDXi0WyBr0oIDXBcMzN8E +qj/xuMNCS/Jy7qTC/LIJo4NgHEBlEubP7bgbJVoh/AFzbbMursurm2w98QKBgDlU +9Vg37mRio2G6lT4u2kimXLfOf6t2t5EJYoGp6PaaW4Zn3qJS/t0yOs3kjmt1aZp6 +Ny/dlICmxXvj7dn/yPVR2vh7D40rTzX/S/pBcT/cUu3GVoxTHzQ4t3NoCt6X2Jph +FRLyPQXSXwfFzA977/31mbZ6RvvQyEfoeAvje37tAoGBAI0myzA1fKF9K0tlWYzV +Csy3pOoSLXv+eaKixOuqVWQYjaXQWGKxY0hl4nuHsh9lHVa/JBETn6O6vKdpAAsY +2cvph0eTThY7L6y1i0qobbD+f1e9AMzlU7n0x6XuqNaSe0Clnu2o8VZ7zt4d+zBE +EZ1JXNWdFMRmhUDeQwcfTNlE +-----END PRIVATE KEY-----` + caCrtPem = `-----BEGIN CERTIFICATE----- +MIIDojCCAoqgAwIBAgIUVZMAZoG4MAmmYCREGR6B9L3CnSswDQYJKoZIhvcNAQEL +BQAwPDELMAkGA1UEBhMCRlIxFzAVBgNVBAoMDk1vbiBFbnRyZXByaXNlMRQwEgYD +VQQDDAtNYSBTdXBlciBDQTAeFw0yNTExMDMxNzE0NDRaFw0zNTExMDExNzE0NDRa +MDwxCzAJBgNVBAYTAkZSMRcwFQYDVQQKDA5Nb24gRW50cmVwcmlzZTEUMBIGA1UE +AwwLTWEgU3VwZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr +c62n/+wkhl1es20//KOB8Ce1ZiTUOnmJ40YiH05bgNyN4myjYxLASq+pmp4eVUqg +EOUWU1ESPzwvM+ZkaaHv77xXWh/pQgdaaTmSJsZf6PQAitNR1/zR/GjZfgGAHXxF +3+d2t8MYwG9i3q+Vh0ERKTaK89PYhqvoGiyxpDZT06QxLTHuR0RP12FfPvFREL0B +5qp+7CAyoWbGkDAWEl2HSVTOnMeS1zwJdgXElKxh0c6ABuOU6QIInQ+eWzz8Hqus +weruseG8AI9xj6sL+etQH8JzQ0ewl10v5bA5mwMIa8aILBCKrAsA6YCtIThu/dh/ +8lCaGfq+Xsbh4gxsYJy1AgMBAAGjgZswgZgwHQYDVR0OBBYEFGr9A0Zqcirk9vvE +Voq7MQ9OnKPGMB8GA1UdIwQYMBaAFGr9A0Zqcirk9vvEVoq7MQ9OnKPGMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMDUGA1UdHwQuMCwwKqAooCaGJGh0 +dHA6Ly9jcmwubW9uLWRvbWFpbmUuY29tL21hLWNhLmNybDANBgkqhkiG9w0BAQsF +AAOCAQEAX8c4XgK0tg+QLyPM/kLrz2h53WhEa+z+7izW6pqIR3YUFzkUycLe/QVJ +y2n3W/tUbsrLpNP6dmfkm8NkjN7ZGskPyKYQQcXqrJtjIo+r63rDiU1IJhe07DjE +rd3247MUw1bfp0XPPWb7my8VXsqJoeC7PwyAaX6suoxodIEYJRiXRpJexYYwrHqQ +n55blja5iPPo57dS/T104wnltGs5/K6IEfOlkh7nS3W6ARem3f+7ZSHRiHOmX24U +w0dqPg3kDiskjn2q+XUt4IKyPYDTdz14p9EwKW+cHkarJdkZyYzbm8219YD4wQF9 +nhNoSJevD/qFbmZF6z6QTxUbDXLHEw== +-----END CERTIFICATE-----` +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + ctx context.Context + cancel context.CancelFunc + testEnv *envtest.Environment + cfg *rest.Config + k8sClient client.Client + caCert *x509.Certificate +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + var err error + err = crloperatorv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = cmv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "config", "crd", "bases"), + filepath.Join("..", "..", "testdata", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + // Retrieve the first found binary directory to allow running tests from IDEs + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + // Create default cert-manager namespace + Expect(k8sClient.Create( + ctx, + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: certManagerNamespace, + }, + }, + )).To(Succeed()) + + // Create default CA secret + Expect(k8sClient.Create( + ctx, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-key-pair", + Namespace: certManagerNamespace, + }, + Data: map[string][]byte{ + "tls.key": []byte(caKeyPem), + "tls.crt": []byte(caCrtPem), + }, + Type: corev1.SecretTypeTLS, + }, + )).To(Succeed()) + + // Parse CA cert + certBlock, _ := pem.Decode([]byte(caCrtPem)) + Expect(certBlock).ToNot(BeNil()) + Expect(certBlock.Type).To(Equal("CERTIFICATE")) + caCert, err = x509.ParseCertificate(certBlock.Bytes) + Expect(err).ToNot(HaveOccurred()) + + // Start the controller + err = (&controller.ManagedCRLReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + CertManagerNamespace: certManagerNamespace, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + err = k8sManager.GetFieldIndexer().IndexField( + context.Background(), + &crloperatorv1alpha1.ManagedCRL{}, + "IssuerRef", + func(rawObj client.Object) []string { + mcrl := rawObj.(*crloperatorv1alpha1.ManagedCRL) + var indexKeys []string + switch mcrl.Spec.IssuerRef.Kind { + case "Issuer": // nolint:goconst // "Issuer" string is clearer + indexKeys = append(indexKeys, fmt.Sprintf("Issuer/%s/%s", mcrl.Namespace, mcrl.Spec.IssuerRef.Name)) + case "ClusterIssuer": // nolint:goconst // "ClusterIssuer" string is clearer + indexKeys = append(indexKeys, fmt.Sprintf("ClusterIssuer/%s", mcrl.Spec.IssuerRef.Name)) + default: + return nil + } + + // Add a reference to the Secret containing the CA certificate and private key + // used to sign the CRL. + if mcrl.Status.ObservedCASecretRef != nil { + indexKeys = append( + indexKeys, + fmt.Sprintf("Secret/%s/%s", mcrl.Status.ObservedCASecretRef.Namespace, mcrl.Status.ObservedCASecretRef.Name), + ) + } + + return indexKeys + }, + ) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred()) + }() +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. +// ENVTEST-based tests depend on specific binaries, usually located in paths set by +// controller-runtime. When running tests directly (e.g., via an IDE) without using +// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. +// +// This function streamlines the process by finding the required binaries, similar to +// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are +// properly set up, run 'make setup-envtest' beforehand. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + logf.Log.Error(err, "Failed to read directory", "path", basePath) + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +}