From 5fca7c1aa948bd80b0ce43e57bdf26c60833f775 Mon Sep 17 00:00:00 2001 From: jfhcagip Date: Mon, 23 Mar 2026 17:40:51 +0100 Subject: [PATCH] Fix CN use in rolebindings --- .../build-test-push-to-test-registry.yml | 2 +- .github/workflows/build-test-release.yml | 2 +- .goreleaser.yml | 20 +- internal/services/rolebindings.go | 19 +- scripts/install_cfssl.sh | 5 +- test/e2e/bootstrap-fixtures-test-e2e.sh | 3 +- test/e2e/e2e_test.go | 283 +++++++++--------- 7 files changed, 182 insertions(+), 152 deletions(-) diff --git a/.github/workflows/build-test-push-to-test-registry.yml b/.github/workflows/build-test-push-to-test-registry.yml index 5ec34606..cbdce045 100644 --- a/.github/workflows/build-test-push-to-test-registry.yml +++ b/.github/workflows/build-test-push-to-test-registry.yml @@ -29,7 +29,7 @@ jobs: id: install - name: Install helm plugin helm-images - run: helm plugin install https://github.com/nikhilsbhat/helm-images + run: helm plugin install --verify=false https://github.com/nikhilsbhat/helm-images - name: Install GoReleaser uses: goreleaser/goreleaser-action@v6 diff --git a/.github/workflows/build-test-release.yml b/.github/workflows/build-test-release.yml index ef1f9761..f8862c6b 100644 --- a/.github/workflows/build-test-release.yml +++ b/.github/workflows/build-test-release.yml @@ -37,7 +37,7 @@ jobs: id: install - name: Install helm plugin helm-images - run: helm plugin install https://github.com/nikhilsbhat/helm-images + run: helm plugin install --verify=false https://github.com/nikhilsbhat/helm-images - name: Install GoReleaser uses: goreleaser/goreleaser-action@v6 diff --git a/.goreleaser.yml b/.goreleaser.yml index 21714314..7a4e7470 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -54,7 +54,7 @@ builds: - -trimpath dockers: - - id: docker-operator + - id: docker-operator-amd64 ids: - operator use: buildx @@ -68,8 +68,26 @@ dockers: - "--label=org.opencontainers.image.url=https://github.com/{{.Env.ORG}}/{{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" + goarch: amd64 image_templates: - "ghcr.io/{{.Env.ORG}}/{{.ProjectName}}-operator:{{.ShortCommit}}-amd64" + - id: docker-operator-arm64 + ids: + - operator + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm64" + - "--build-arg=BINARYNAME=operator" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}-operator" + - "--label=org.opencontainers.image.source=https://github.com/{{.Env.ORG}}/{{.ProjectName}}" + - "--label=org.opencontainers.image.url=https://github.com/{{.Env.ORG}}/{{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + goarch: arm64 + image_templates: + - "ghcr.io/{{.Env.ORG}}/{{.ProjectName}}-operator:{{.ShortCommit}}-arm64" - id: docker-api ids: diff --git a/internal/services/rolebindings.go b/internal/services/rolebindings.go index 19fdc445..51eb6d31 100644 --- a/internal/services/rolebindings.go +++ b/internal/services/rolebindings.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "regexp" "strings" "github.com/ca-gip/kubi/internal/utils" @@ -115,17 +116,17 @@ func generateRoleBindings(project *cagipv1.Project, defaultServiceAccountRole st { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", - Name: strings.ToUpper(utils.Config.Ldap.AppMasterGroupBase), // the equivalent of application master (appops) + Name: getGroupCN(utils.Config.Ldap.AppMasterGroupBase), // the equivalent of application master (appops) }, { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", - Name: strings.ToUpper(utils.Config.Ldap.CustomerOpsGroupBase), // the equivalent of application master (customerops) + Name: getGroupCN(utils.Config.Ldap.CustomerOpsGroupBase), // the equivalent of application master (customerops) }, { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", - Name: strings.ToUpper(utils.Config.Ldap.OpsMasterGroupBase), // the equivalent of ops master + Name: getGroupCN(utils.Config.Ldap.OpsMasterGroupBase), // the equivalent of ops master }, }, }, @@ -141,7 +142,7 @@ func generateRoleBindings(project *cagipv1.Project, defaultServiceAccountRole st { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", - Name: strings.ToUpper(utils.Config.Ldap.ViewerGroupBase), + Name: getGroupCN(utils.Config.Ldap.ViewerGroupBase), }, }, }, @@ -184,3 +185,13 @@ func generateRoleBindings(project *cagipv1.Project, defaultServiceAccountRole st } return nil } + +func getGroupCN(ldapDN string) string { + // Example: "CN=group1,OU=groups,DC=example,DC=com" -> "group1" + pat := regexp.MustCompile(`CN=([^,]+)`) + matches := pat.FindStringSubmatch(ldapDN) + if len(matches) > 1 { + return strings.ToUpper(matches[1]) + } + return strings.ToUpper(ldapDN) +} diff --git a/scripts/install_cfssl.sh b/scripts/install_cfssl.sh index 15242142..c6b1b2ab 100755 --- a/scripts/install_cfssl.sh +++ b/scripts/install_cfssl.sh @@ -20,10 +20,11 @@ sudo mkdir -p "$CFSSL_DIR" docker pull cloudflare/cfssl # Run CFSSL inside a Docker container to download and store binaries in /opt/cfssl +PLATFORM="$(go env GOARCH)" docker run --rm -v "$CFSSL_DIR":/cfssl cloudflare/cfssl \ - sh -c "wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssljson_${VNUMBER}_linux_amd64 -O /cfssl/cfssljson && \ + sh -c "wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssljson_${VNUMBER}_linux_${PLATFORM} -O /cfssl/cfssljson && \ chmod +x /cfssl/cfssljson && \ - wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O /cfssl/cfssl && \ + wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_${PLATFORM} -O /cfssl/cfssl && \ chmod +x /cfssl/cfssl && \ cfssljson -version && \ /cfssl/cfssl -version" diff --git a/test/e2e/bootstrap-fixtures-test-e2e.sh b/test/e2e/bootstrap-fixtures-test-e2e.sh index 02d83f73..5510f1d8 100755 --- a/test/e2e/bootstrap-fixtures-test-e2e.sh +++ b/test/e2e/bootstrap-fixtures-test-e2e.sh @@ -91,7 +91,8 @@ EOF # Set deployments images COMMIT_SHA="$(git rev-parse --short HEAD)" -IMG_VERSION="${COMMIT_SHA}-amd64" +PLATFORM="$(go env GOARCH)" +IMG_VERSION="${COMMIT_SHA}-${PLATFORM}" IMG_REPO="ghcr.io/ca-gip" ORG=ca-gip goreleaser release --clean --snapshot diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index cffe335c..81d31a74 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -26,7 +26,6 @@ import ( "os/exec" "path/filepath" "regexp" - "strings" "time" "github.com/ca-gip/kubi/internal/services" @@ -305,9 +304,9 @@ var _ = Describe("Manager", Ordered, func() { {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "ops:masters"}, {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: testProject.Spec.SourceEntity}, - {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: strings.ToUpper(kubiConfig.Data["LDAP_APP_GROUPBASE"])}, - {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: strings.ToUpper(kubiConfig.Data["LDAP_CUSTOMER_OPS_GROUPBASE"])}, - {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: strings.ToUpper(kubiConfig.Data["LDAP_OPS_GROUPBASE"])}, + {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "CAGIP_MEMBERS"}, + {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "DL_KUB_CAGIPHP_OPS"}, + {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "CLOUDOPS_KUBERNETES"}, } g.Expect(nsAdminSa.Subjects).To(Equal(nsAdminSaSubjects), "for rb namespaced-admin, expected binding to groups projet-toto-development:admin application:master and ops:masters - TODO") @@ -322,7 +321,7 @@ var _ = Describe("Manager", Ordered, func() { viewSaSubjects := []rbacv1.Subject{ {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "application:view"}, - {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: strings.ToUpper(kubiConfig.Data["LDAP_VIEWER_GROUPBASE"])}, + {APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "DL_KUB_CAGIPHP_VIEW"}, } g.Expect(viewSa.Subjects).To(Equal(viewSaSubjects), "for rb view, expected binding to the group application:view - TODO") } @@ -764,52 +763,52 @@ var _ = Describe("Manager", Ordered, func() { // Remove the kerrors import and replace the checks - Context("Project deletion", func() { - It("should delete all project resources when project is deleted", func() { - By("verifying that all resources exist before deletion") - verifyAllResourcesExist := func(g Gomega) { - // Check namespace exists - _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), testProjectName, v1.GetOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Namespace should exist before deletion") - - // Check service account exists - _, err = clientset.CoreV1().ServiceAccounts(testProjectName).Get(context.TODO(), "service", v1.GetOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Service account should exist before deletion") - - // Check role bindings exist - rbs, err := clientset.RbacV1().RoleBindings(testProjectName).List(context.TODO(), v1.ListOptions{ - LabelSelector: "creator=kubi", - }) - g.Expect(err).NotTo(HaveOccurred(), "Should be able to list role bindings") - g.Expect(len(rbs.Items)).To(BeNumerically(">", 0), "Role bindings should exist before deletion") - - // Check network policy exists (if network policy is enabled) - _, err = clientset.NetworkingV1().NetworkPolicies(testProjectName).Get(context.TODO(), "kubi-default", v1.GetOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Network policy should exist before deletion") - } - - By("deleting the project") - deleteProject := func(g Gomega) { - err := kubiclient.CagipV1().Projects().Delete(context.TODO(), testProjectName, v1.DeleteOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Failed to delete project") - } - - By("verifying that all resources are cleaned up after project deletion") - verifyAllResourcesDeleted := func(g Gomega) { - // Check namespace is deleted - _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), testProjectName, v1.GetOptions{}) - g.Expect(err).To(HaveOccurred(), "Namespace should be deleted") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Namespace should return not found error") - - // Check service account is deleted - _, err = clientset.CoreV1().ServiceAccounts(testProjectName).Get(context.TODO(), "service", v1.GetOptions{}) - g.Expect(err).To(HaveOccurred(), "Service account should be deleted") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Service account should return not found error") - - // Check network policy is deleted - _, err = clientset.NetworkingV1().NetworkPolicies(testProjectName).Get(context.TODO(), "kubi-default", v1.GetOptions{}) - g.Expect(err).To(HaveOccurred(), "Network policy should be deleted") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Network policy should return not found error") + Context("Project deletion", func() { + It("should delete all project resources when project is deleted", func() { + By("verifying that all resources exist before deletion") + verifyAllResourcesExist := func(g Gomega) { + // Check namespace exists + _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), testProjectName, v1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Namespace should exist before deletion") + + // Check service account exists + _, err = clientset.CoreV1().ServiceAccounts(testProjectName).Get(context.TODO(), "service", v1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Service account should exist before deletion") + + // Check role bindings exist + rbs, err := clientset.RbacV1().RoleBindings(testProjectName).List(context.TODO(), v1.ListOptions{ + LabelSelector: "creator=kubi", + }) + g.Expect(err).NotTo(HaveOccurred(), "Should be able to list role bindings") + g.Expect(len(rbs.Items)).To(BeNumerically(">", 0), "Role bindings should exist before deletion") + + // Check network policy exists (if network policy is enabled) + _, err = clientset.NetworkingV1().NetworkPolicies(testProjectName).Get(context.TODO(), "kubi-default", v1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Network policy should exist before deletion") + } + + By("deleting the project") + deleteProject := func(g Gomega) { + err := kubiclient.CagipV1().Projects().Delete(context.TODO(), testProjectName, v1.DeleteOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Failed to delete project") + } + + By("verifying that all resources are cleaned up after project deletion") + verifyAllResourcesDeleted := func(g Gomega) { + // Check namespace is deleted + _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), testProjectName, v1.GetOptions{}) + g.Expect(err).To(HaveOccurred(), "Namespace should be deleted") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Namespace should return not found error") + + // Check service account is deleted + _, err = clientset.CoreV1().ServiceAccounts(testProjectName).Get(context.TODO(), "service", v1.GetOptions{}) + g.Expect(err).To(HaveOccurred(), "Service account should be deleted") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Service account should return not found error") + + // Check network policy is deleted + _, err = clientset.NetworkingV1().NetworkPolicies(testProjectName).Get(context.TODO(), "kubi-default", v1.GetOptions{}) + g.Expect(err).To(HaveOccurred(), "Network policy should be deleted") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Network policy should return not found error") //Check role bindings are deleted rbs, err := clientset.RbacV1().RoleBindings(testProjectName).List(context.TODO(), v1.ListOptions{ LabelSelector: "creator=kubi", @@ -818,97 +817,97 @@ var _ = Describe("Manager", Ordered, func() { g.Expect(len(rbs.Items)).To(Equal(0), "Role bindings should be deleted") } - By("verifying project is removed from Kubernetes") - verifyProjectDeleted := func(g Gomega) { - _, err := kubiclient.CagipV1().Projects().Get(context.TODO(), testProjectName, v1.GetOptions{}) - g.Expect(err).To(HaveOccurred(), "Project should be deleted from Kubernetes") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Project should return not found error") - } - - Eventually(verifyAllResourcesExist).Should(Succeed()) - Eventually(deleteProject).Should(Succeed()) - Eventually(verifyAllResourcesDeleted).Should(Succeed()) - Eventually(verifyProjectDeleted).Should(Succeed()) - }) - - It("should handle deletion of non-existent project gracefully", func() { - By("attempting to delete a non-existent project") - deleteNonExistentProject := func(g Gomega) { - err := kubiclient.CagipV1().Projects().Delete(context.TODO(), "non-existent-project", v1.DeleteOptions{}) - g.Expect(err).To(HaveOccurred(), "Deleting non-existent project should return error") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Deleting non-existent project should return NotFound error") - } - - Eventually(deleteNonExistentProject).Should(Succeed()) - }) - - It("should handle partial deletion failures gracefully", func() { - By("creating a test project for deletion testing") - createTestProject := func(g Gomega) { - testProject := &kubiv1.Project{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-deletion-project", - Labels: map[string]string{ - "creator": "kubi", - }, - }, - Spec: kubiv1.ProjectSpec{ - Environment: "development", - Project: "test-deletion", - SourceEntity: "TEST_GROUP", - Stages: []string{"scratch"}, - Tenant: "cagip", - }, - } - - _, err := kubiclient.CagipV1().Projects().Create(context.TODO(), testProject, v1.CreateOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Failed to create test project") - } - - By("waiting for resources to be created") - waitForResourceCreation := func(g Gomega) { - // Wait for namespace to be created - _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), "test-deletion-project", v1.GetOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Test namespace should be created") - - // Wait for service account to be created - _, err = clientset.CoreV1().ServiceAccounts("test-deletion-project").Get(context.TODO(), "service", v1.GetOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Test service account should be created") - } - - By("manually deleting some resources to simulate partial deletion") - simulatePartialDeletion := func(g Gomega) { - // Delete the service account manually to simulate a scenario where not all resources can be deleted - err := clientset.CoreV1().ServiceAccounts("test-deletion-project").Delete(context.TODO(), "service", v1.DeleteOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Should be able to manually delete service account") - } - - By("deleting the project and verifying graceful handling") - deleteProjectGracefully := func(g Gomega) { - err := kubiclient.CagipV1().Projects().Delete(context.TODO(), "test-deletion-project", v1.DeleteOptions{}) - g.Expect(err).NotTo(HaveOccurred(), "Should be able to delete project even with missing resources") - } - - By("verifying cleanup completes despite missing resources") - verifyGracefulCleanup := func(g Gomega) { - // The namespace should still be deleted - _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), "test-deletion-project", v1.GetOptions{}) - g.Expect(err).To(HaveOccurred(), "Namespace should be deleted despite partial failures") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Namespace should return not found error") - - // Project should be removed - _, err = kubiclient.CagipV1().Projects().Get(context.TODO(), "test-deletion-project", v1.GetOptions{}) - g.Expect(err).To(HaveOccurred(), "Project should be deleted") - g.Expect(err.Error()).To(ContainSubstring("not found"), "Project should return not found error") - } - - Eventually(createTestProject).Should(Succeed()) - Eventually(waitForResourceCreation).Should(Succeed()) - Eventually(simulatePartialDeletion).Should(Succeed()) - Eventually(deleteProjectGracefully).Should(Succeed()) - Eventually(verifyGracefulCleanup).Should(Succeed()) - }) - }) + By("verifying project is removed from Kubernetes") + verifyProjectDeleted := func(g Gomega) { + _, err := kubiclient.CagipV1().Projects().Get(context.TODO(), testProjectName, v1.GetOptions{}) + g.Expect(err).To(HaveOccurred(), "Project should be deleted from Kubernetes") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Project should return not found error") + } + + Eventually(verifyAllResourcesExist).Should(Succeed()) + Eventually(deleteProject).Should(Succeed()) + Eventually(verifyAllResourcesDeleted).Should(Succeed()) + Eventually(verifyProjectDeleted).Should(Succeed()) + }) + + It("should handle deletion of non-existent project gracefully", func() { + By("attempting to delete a non-existent project") + deleteNonExistentProject := func(g Gomega) { + err := kubiclient.CagipV1().Projects().Delete(context.TODO(), "non-existent-project", v1.DeleteOptions{}) + g.Expect(err).To(HaveOccurred(), "Deleting non-existent project should return error") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Deleting non-existent project should return NotFound error") + } + + Eventually(deleteNonExistentProject).Should(Succeed()) + }) + + It("should handle partial deletion failures gracefully", func() { + By("creating a test project for deletion testing") + createTestProject := func(g Gomega) { + testProject := &kubiv1.Project{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-deletion-project", + Labels: map[string]string{ + "creator": "kubi", + }, + }, + Spec: kubiv1.ProjectSpec{ + Environment: "development", + Project: "test-deletion", + SourceEntity: "TEST_GROUP", + Stages: []string{"scratch"}, + Tenant: "cagip", + }, + } + + _, err := kubiclient.CagipV1().Projects().Create(context.TODO(), testProject, v1.CreateOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Failed to create test project") + } + + By("waiting for resources to be created") + waitForResourceCreation := func(g Gomega) { + // Wait for namespace to be created + _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), "test-deletion-project", v1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Test namespace should be created") + + // Wait for service account to be created + _, err = clientset.CoreV1().ServiceAccounts("test-deletion-project").Get(context.TODO(), "service", v1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Test service account should be created") + } + + By("manually deleting some resources to simulate partial deletion") + simulatePartialDeletion := func(g Gomega) { + // Delete the service account manually to simulate a scenario where not all resources can be deleted + err := clientset.CoreV1().ServiceAccounts("test-deletion-project").Delete(context.TODO(), "service", v1.DeleteOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Should be able to manually delete service account") + } + + By("deleting the project and verifying graceful handling") + deleteProjectGracefully := func(g Gomega) { + err := kubiclient.CagipV1().Projects().Delete(context.TODO(), "test-deletion-project", v1.DeleteOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "Should be able to delete project even with missing resources") + } + + By("verifying cleanup completes despite missing resources") + verifyGracefulCleanup := func(g Gomega) { + // The namespace should still be deleted + _, err := clientset.CoreV1().Namespaces().Get(context.TODO(), "test-deletion-project", v1.GetOptions{}) + g.Expect(err).To(HaveOccurred(), "Namespace should be deleted despite partial failures") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Namespace should return not found error") + + // Project should be removed + _, err = kubiclient.CagipV1().Projects().Get(context.TODO(), "test-deletion-project", v1.GetOptions{}) + g.Expect(err).To(HaveOccurred(), "Project should be deleted") + g.Expect(err.Error()).To(ContainSubstring("not found"), "Project should return not found error") + } + + Eventually(createTestProject).Should(Succeed()) + Eventually(waitForResourceCreation).Should(Succeed()) + Eventually(simulatePartialDeletion).Should(Succeed()) + Eventually(deleteProjectGracefully).Should(Succeed()) + Eventually(verifyGracefulCleanup).Should(Succeed()) + }) + }) }) // serviceAccountToken returns a token for the specified service account in the given namespace.