From 8af72f42c8aec01db891e239039cec745fe8ae29 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 09:09:40 +0000 Subject: [PATCH 01/16] .github: Move lint to pre-merge workflow Signed-off-by: Teddy Andrieux --- .../workflows/{lint.yml => pre-merge.yaml} | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename .github/workflows/{lint.yml => pre-merge.yaml} (54%) diff --git a/.github/workflows/lint.yml b/.github/workflows/pre-merge.yaml similarity index 54% rename from .github/workflows/lint.yml rename to .github/workflows/pre-merge.yaml index 86e3845..ba1b322 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/pre-merge.yaml @@ -1,23 +1,23 @@ -name: Lint +name: "Pre Merge" on: - push: + workflow_dispatch: + pull_request: + branches: + - main jobs: lint: - 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 + - name: Setup go uses: actions/setup-go@v5 with: go-version-file: go.mod - - - name: Run linter + - name: Run linters uses: golangci/golangci-lint-action@v8 with: - version: v2.1.0 + version: v2.5.0 From 5124d65ee69017490279722b8efb8ead2997c239 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 09:14:40 +0000 Subject: [PATCH 02/16] .github: Ensure generated go is up to date in CI Signed-off-by: Teddy Andrieux --- .github/workflows/pre-merge.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml index ba1b322..b46ee1a 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -8,6 +8,22 @@ on: - main jobs: + 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: From bd82c20b93973a76820b7c3860afab59317a4a13 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 09:42:00 +0000 Subject: [PATCH 03/16] internal: Remove tests for now (it will be done in integration tests) Signed-off-by: Teddy Andrieux --- internal/controller/managedcrl_controller_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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. }) + */ }) }) From 31b815ff39e971d965323b6fe2824aed9781ee30 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 09:53:26 +0000 Subject: [PATCH 04/16] .devcontainer: Add kind and kubectl in Dockerfile Signed-off-by: Teddy Andrieux --- .devcontainer/Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 From ca09826db2b5bbe8a9d52ef063c8660d74bc788d Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 09:20:14 +0000 Subject: [PATCH 05/16] .github: Move test workflow to pre-merge Signed-off-by: Teddy Andrieux --- .github/workflows/pre-merge.yaml | 4 ++++ .github/workflows/test.yml | 9 +++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml index b46ee1a..3c4a5ec 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -37,3 +37,7 @@ jobs: uses: golangci/golangci-lint-action@v8 with: version: v2.5.0 + + test: + uses: ./.github/workflows/test.yml + secrets: inherit 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: From 6bb54c0a157ce298ecfeac28b1ef4d9be0504478 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 09:35:07 +0000 Subject: [PATCH 06/16] .github: Move e2e-test workflow to pre-merge Signed-off-by: Teddy Andrieux --- .github/workflows/pre-merge.yaml | 4 ++++ .github/workflows/test-e2e.yml | 15 +++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml index 3c4a5ec..4b37b9f 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -41,3 +41,7 @@ jobs: test: uses: ./.github/workflows/test.yml secrets: inherit + + test-e2e: + uses: ./.github/workflows/test-e2e.yml + secrets: inherit diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 68fd1ed..0335678 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,25 +1,24 @@ 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 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 From 9dcf4cb8ae8720fe6dc7eaf76fe83e79150bceb2 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 14:42:50 +0000 Subject: [PATCH 07/16] .github: Add build of container image in pre-merge Signed-off-by: Teddy Andrieux --- .github/workflows/build.yaml | 62 ++++++++++++++++++++++++++++++++ .github/workflows/pre-merge.yaml | 8 +++++ 2 files changed, 70 insertions(+) create mode 100644 .github/workflows/build.yaml 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/pre-merge.yaml b/.github/workflows/pre-merge.yaml index 4b37b9f..7ac8051 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -8,6 +8,14 @@ on: - 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: From e4cc7a7acc5a96ed2b6e3a04d5a22ae998e5fd28 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 14:43:42 +0000 Subject: [PATCH 08/16] .github: Add post-merge workflow Signed-off-by: Teddy Andrieux --- .github/workflows/post-merge.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/post-merge.yaml 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 From d297f6c241d44b3d00e484509bba34e9add56b65 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 14:44:49 +0000 Subject: [PATCH 09/16] .github: Add promote workflow Signed-off-by: Teddy Andrieux --- .github/workflows/promote.yaml | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/promote.yaml 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 }} From 20c24006412ddc9e369c66e53e0f60c6ab116b79 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 3 Nov 2025 14:47:21 +0000 Subject: [PATCH 10/16] .github: Add release workflow Signed-off-by: Teddy Andrieux --- .github/workflows/release.yaml | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .github/workflows/release.yaml 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 }}" From 88defb7ac9a72f33e0c22ebfb0c98766e1f0f23e Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 4 Nov 2025 09:14:47 +0000 Subject: [PATCH 11/16] chore: Fix bug revoked certificates not removed if the list is cleared --- internal/controller/managedcrl_controller.go | 3 +++ 1 file changed, 3 insertions(+) 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) { From 1a88a4c873ace16ca341cc53484c765362a82aae Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 4 Nov 2025 09:17:08 +0000 Subject: [PATCH 12/16] test: Add integration tests for ManagedCRL secret creation Signed-off-by: Teddy Andrieux --- .gitignore | 1 + Makefile | 12 +- .../integration/managedcrl_controller_test.go | 319 ++++++++++++++++++ test/integration/suite_test.go | 267 +++++++++++++++ 4 files changed, 598 insertions(+), 1 deletion(-) create mode 100644 test/integration/managedcrl_controller_test.go create mode 100644 test/integration/suite_test.go 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/test/integration/managedcrl_controller_test.go b/test/integration/managedcrl_controller_test.go new file mode 100644 index 0000000..0a62508 --- /dev/null +++ b/test/integration/managedcrl_controller_test.go @@ -0,0 +1,319 @@ +/* +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" + cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/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: "nominal-secret-only-issuer", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + IssuerRef: cmmetav1.IssuerReference{ + Name: "test-issuer", + Kind: "Issuer", + }, + }, + shouldError: false, + shouldExposePod: false, + shouldExposeIngress: false, + shouldConfigureIssuer: false, + }, { + name: "nominal-secret-only-clusterissuer", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + IssuerRef: cmmetav1.IssuerReference{ + Name: "test-issuer", + Kind: "ClusterIssuer", + }, + }, + shouldError: false, + shouldExposePod: false, + shouldExposeIngress: false, + shouldConfigureIssuer: false, + }, + } +) + +func toTableEntry(tcs []mcrlTestCase) []TableEntry { + entries := make([]TableEntry, len(tcs)) + for i, tc := range tcs { + entries[i] = 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 a norminal secret only 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) + + 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) + + 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) + + 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) + + 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) { + By("checking the ManagedCRL becomes Secret properly setup") + checkSecret(mcrlRef) + + if tc.shouldExposePod { + Expect(false).To(BeTrue()) // TODO + } 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 { + Expect(false).To(BeTrue()) // TODO + } 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 { + Expect(false).To(BeTrue()) // TODO + } 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)) + } + } +} diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go new file mode 100644 index 0000000..addd8b6 --- /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": + indexKeys = append(indexKeys, fmt.Sprintf("Issuer/%s/%s", mcrl.Namespace, mcrl.Spec.IssuerRef.Name)) + case "ClusterIssuer": + 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 "" +} From 9ecc943b37836900f740037b67b9a6115084d6c6 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 4 Nov 2025 13:29:51 +0000 Subject: [PATCH 13/16] test: Add integration test for ManagedCRL with exposed pod Signed-off-by: Teddy Andrieux --- .../integration/managedcrl_controller_test.go | 152 +++++++++++++++--- 1 file changed, 130 insertions(+), 22 deletions(-) diff --git a/test/integration/managedcrl_controller_test.go b/test/integration/managedcrl_controller_test.go index 0a62508..c70d779 100644 --- a/test/integration/managedcrl_controller_test.go +++ b/test/integration/managedcrl_controller_test.go @@ -51,37 +51,53 @@ type mcrlTestCase struct { var ( testCases = []mcrlTestCase{ { - name: "nominal-secret-only-issuer", + name: "nominal-secret-only", spec: crloperatorv1alpha1.ManagedCRLSpec{ IssuerRef: cmmetav1.IssuerReference{ Name: "test-issuer", - Kind: "Issuer", }, }, - shouldError: false, - shouldExposePod: false, - shouldExposeIngress: false, - shouldConfigureIssuer: false, }, { - name: "nominal-secret-only-clusterissuer", + name: "nominal-exposed-only", spec: crloperatorv1alpha1.ManagedCRLSpec{ IssuerRef: cmmetav1.IssuerReference{ Name: "test-issuer", - Kind: "ClusterIssuer", + }, + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), + }, + }, + shouldExposePod: true, + }, { + name: "nominal-exposed-with-custom-im", + spec: crloperatorv1alpha1.ManagedCRLSpec{ + IssuerRef: cmmetav1.IssuerReference{ + Name: "test-issuer", + }, + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Image: &crloperatorv1alpha1.ImageSpec{Repository: ptr.To("custom/repo"), Tag: ptr.To("v1.2.3")}, + Internal: ptr.To(false), }, }, - shouldError: false, - shouldExposePod: false, - shouldExposeIngress: false, - shouldConfigureIssuer: false, + shouldExposePod: true, }, } ) func toTableEntry(tcs []mcrlTestCase) []TableEntry { - entries := make([]TableEntry, len(tcs)) - for i, tc := range tcs { - entries[i] = Entry(fmt.Sprintf("ManagedCRL %s", tc.name), tc) + entries := []TableEntry{} + for _, tc := range tcs { + // Always add one entry for Issuer and one for ClusterIssuer + name := tc.name + 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 } @@ -165,7 +181,7 @@ var _ = Describe("ManagedCRL Controller", func() { }) DescribeTableSubtree("should reconcile various ManagedCRL resources as expected", func(tc mcrlTestCase) { - It("should successfully reconcile a norminal secret only resource", func() { + It("should successfully reconcile resource", func() { typeNamespacedName := types.NamespacedName{ Name: tc.name, Namespace: testNamespace, @@ -185,7 +201,7 @@ var _ = Describe("ManagedCRL Controller", func() { return } Expect(err).ToNot(HaveOccurred()) - checkAllReady(typeNamespacedName, tc) + checkAllReady(typeNamespacedName, tc, true) By("adding a revoked certificate to the CRL and checking the update is reflected") retrieved := &crloperatorv1alpha1.ManagedCRL{} @@ -197,7 +213,7 @@ var _ = Describe("ManagedCRL Controller", func() { }, } Expect(k8sClient.Update(ctx, retrieved)).To(Succeed()) - checkAllReady(typeNamespacedName, tc) + checkAllReady(typeNamespacedName, tc, false) By("changing the reason code of a revoked certificate") Expect(k8sClient.Get(ctx, typeNamespacedName, retrieved)).To(Succeed()) @@ -208,13 +224,13 @@ var _ = Describe("ManagedCRL Controller", func() { }, } Expect(k8sClient.Update(ctx, retrieved)).To(Succeed()) - checkAllReady(typeNamespacedName, tc) + 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) + checkAllReady(typeNamespacedName, tc, false) By("deleting the ManagedCRL") Expect(k8sClient.Delete(ctx, retrieved)).To(Succeed()) @@ -228,12 +244,13 @@ var _ = Describe("ManagedCRL Controller", func() { }) }) -func checkAllReady(mcrlRef types.NamespacedName, tc mcrlTestCase) { +func checkAllReady(mcrlRef types.NamespacedName, tc mcrlTestCase, podShouldRestart bool) { By("checking the ManagedCRL becomes Secret properly setup") checkSecret(mcrlRef) if tc.shouldExposePod { - Expect(false).To(BeTrue()) // TODO + By("checking the ManagedCRL becomes PodExposed properly setup") + checkExposePod(mcrlRef, podShouldRestart) } else { By("checking no PodExposed status is set") retrieved := &crloperatorv1alpha1.ManagedCRL{} @@ -317,3 +334,94 @@ func checkSecret(mcrlRef types.NamespacedName) { } } } + +// 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), + }))) +} From ba809cac58013b6fbd3b8da42c75ac8ed947cabc Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 4 Nov 2025 14:25:10 +0000 Subject: [PATCH 14/16] test: Add integration tests for ManagedCRL ingress creation Signed-off-by: Teddy Andrieux --- .../integration/managedcrl_controller_test.go | 125 ++++++++++++++++-- 1 file changed, 111 insertions(+), 14 deletions(-) diff --git a/test/integration/managedcrl_controller_test.go b/test/integration/managedcrl_controller_test.go index c70d779..08fdb23 100644 --- a/test/integration/managedcrl_controller_test.go +++ b/test/integration/managedcrl_controller_test.go @@ -32,7 +32,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" - cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -51,37 +50,88 @@ type mcrlTestCase struct { var ( testCases = []mcrlTestCase{ { - name: "nominal-secret-only", + name: "secret-only", + spec: crloperatorv1alpha1.ManagedCRLSpec{}, + }, { + name: "exposed-only", spec: crloperatorv1alpha1.ManagedCRLSpec{ - IssuerRef: cmmetav1.IssuerReference{ - Name: "test-issuer", + Expose: &crloperatorv1alpha1.CRLExposeSpec{ + Enabled: true, + Internal: ptr.To(false), }, }, + shouldExposePod: true, }, { - name: "nominal-exposed-only", + name: "exposed-with-custom-im", spec: crloperatorv1alpha1.ManagedCRLSpec{ - IssuerRef: cmmetav1.IssuerReference{ - Name: "test-issuer", + 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: "nominal-exposed-with-custom-im", + name: "ingress-only-hostname", spec: crloperatorv1alpha1.ManagedCRLSpec{ - IssuerRef: cmmetav1.IssuerReference{ - Name: "test-issuer", + 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, - Image: &crloperatorv1alpha1.ImageSpec{Repository: ptr.To("custom/repo"), Tag: ptr.To("v1.2.3")}, Internal: ptr.To(false), + Ingress: &crloperatorv1alpha1.IngressSpec{ + IPAddresses: []crloperatorv1alpha1.IPAddress{ + "10.11.12.13", + "20.21.22.23", + }, + }, }, }, - shouldExposePod: true, + 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, }, } ) @@ -91,6 +141,8 @@ func toTableEntry(tcs []mcrlTestCase) []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)) @@ -258,7 +310,8 @@ func checkAllReady(mcrlRef types.NamespacedName, tc mcrlTestCase, podShouldResta Expect(retrieved.Status.PodExposed).To(BeNil()) } if tc.shouldExposeIngress { - Expect(false).To(BeTrue()) // TODO + By("checking the ManagedCRL becomes IngressExposed properly setup") + checkIngress(mcrlRef) } else { By("checking no IngressExposed status is set") retrieved := &crloperatorv1alpha1.ManagedCRL{} @@ -266,7 +319,7 @@ func checkAllReady(mcrlRef types.NamespacedName, tc mcrlTestCase, podShouldResta Expect(retrieved.Status.IngressExposed).To(BeNil()) } if tc.shouldConfigureIssuer { - Expect(false).To(BeTrue()) // TODO + Expect(true).To(BeTrue()) // TODO } else { By("checking no IssuerConfigured status is set") retrieved := &crloperatorv1alpha1.ManagedCRL{} @@ -425,3 +478,47 @@ func checkExposePod(mcrlRef types.NamespacedName, shouldRestart bool) { "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()) + } +} From 9b0e48c1ed832ae2ce6e8c9b1870de04563bc6db Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 4 Nov 2025 14:37:05 +0000 Subject: [PATCH 15/16] test: Add integration tests for ManagedCRL issuer configuration Signed-off-by: Teddy Andrieux --- .../integration/managedcrl_controller_test.go | 94 ++++++++++++++++++- test/integration/suite_test.go | 4 +- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/test/integration/managedcrl_controller_test.go b/test/integration/managedcrl_controller_test.go index 08fdb23..29039a2 100644 --- a/test/integration/managedcrl_controller_test.go +++ b/test/integration/managedcrl_controller_test.go @@ -132,6 +132,51 @@ var ( 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, }, } ) @@ -319,7 +364,8 @@ func checkAllReady(mcrlRef types.NamespacedName, tc mcrlTestCase, podShouldResta Expect(retrieved.Status.IngressExposed).To(BeNil()) } if tc.shouldConfigureIssuer { - Expect(true).To(BeTrue()) // TODO + By("checking the ManagedCRL becomes IssuerConfigured properly setup") + checkIssuerConfigured(mcrlRef) } else { By("checking no IssuerConfigured status is set") retrieved := &crloperatorv1alpha1.ManagedCRL{} @@ -522,3 +568,49 @@ func checkIngress(mcrlRef types.NamespacedName) { 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 index addd8b6..5506df2 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -207,9 +207,9 @@ var _ = BeforeSuite(func() { mcrl := rawObj.(*crloperatorv1alpha1.ManagedCRL) var indexKeys []string switch mcrl.Spec.IssuerRef.Kind { - case "Issuer": + case "Issuer": // nolint:goconst // "Issuer" string is clearer indexKeys = append(indexKeys, fmt.Sprintf("Issuer/%s/%s", mcrl.Namespace, mcrl.Spec.IssuerRef.Name)) - case "ClusterIssuer": + case "ClusterIssuer": // nolint:goconst // "ClusterIssuer" string is clearer indexKeys = append(indexKeys, fmt.Sprintf("ClusterIssuer/%s", mcrl.Spec.IssuerRef.Name)) default: return nil From c8c50c79e26a4b3f1b25f2359363cee5a91668a2 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 4 Nov 2025 14:42:44 +0000 Subject: [PATCH 16/16] .github: Disable e2e tests for now Signed-off-by: Teddy Andrieux --- .github/workflows/test-e2e.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 0335678..46215b4 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -6,6 +6,8 @@ on: jobs: test-e2e: runs-on: ubuntu-24.04 + # Disable e2e tests since there is none for now + if: false steps: - name: Checkout uses: actions/checkout@v4