diff --git a/test/openshift/e2e/ginkgo/fixture/agent/fixture.go b/test/openshift/e2e/ginkgo/fixture/agent/fixture.go new file mode 100644 index 00000000000..40455432f38 --- /dev/null +++ b/test/openshift/e2e/ginkgo/fixture/agent/fixture.go @@ -0,0 +1,564 @@ +package argocdagentprincipal + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "math/big" + "net" + "sort" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/argoproj-labs/argocd-operator/common" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + osFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/os" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" +) + +type PrincipalResources struct { + PrincipalNamespaceName string + ArgoCDAgentPrincipalName string + ArgoCDName string + ServiceAccount *corev1.ServiceAccount + Role *rbacv1.Role + RoleBinding *rbacv1.RoleBinding + ClusterRole *rbacv1.ClusterRole + ClusterRoleBinding *rbacv1.ClusterRoleBinding + PrincipalDeployment *appsv1.Deployment + PrincipalRoute *routev1.Route + + ServicesToDelete []string +} + +type PrincipalSecretsConfig struct { + PrincipalNamespaceName string + PrincipalServiceName string + ResourceProxyServiceName string + JWTSecretName string + PrincipalTLSSecretName string + RootCASecretName string + ResourceProxyTLSSecretName string + AdditionalPrincipalSANs []string + AdditionalResourceProxySANs []string +} + +type AgentSecretsConfig struct { + AgentNamespace *corev1.Namespace + PrincipalNamespaceName string + PrincipalRootCASecretName string + AgentRootCASecretName string + ClientTLSSecretName string + ClientCommonName string + ClientDNSNames []string +} + +type ClusterRegistrationSecretConfig struct { + PrincipalNamespaceName string + AgentNamespaceName string + AgentName string + ResourceProxyServiceName string + ResourceProxyPort int32 + PrincipalRootCASecretName string + AgentTLSSecretName string + Server string +} + +type AgentSecretNames struct { + JWTSecretName string + PrincipalTLSSecretName string + RootCASecretName string + ResourceProxyTLSSecretName string + RedisInitialPasswordSecretName string +} + +type VerifyExpectedResourcesExistParams struct { + Namespace *corev1.Namespace + ArgoCDAgentPrincipalName string + ArgoCDName string + ServiceAccount *corev1.ServiceAccount + Role *rbacv1.Role + RoleBinding *rbacv1.RoleBinding + ClusterRole *rbacv1.ClusterRole + ClusterRoleBinding *rbacv1.ClusterRoleBinding + PrincipalDeployment *appsv1.Deployment + PrincipalRoute *routev1.Route + SecretNames AgentSecretNames + ServiceNames []string + DeploymentNames []string + ExpectRoute *bool +} + +func VerifyResourcesDeleted(resources PrincipalResources) { + + By("verifying resources are deleted for principal pod") + + Eventually(resources.ServiceAccount).Should(k8sFixture.NotExistByName()) + Eventually(resources.Role).Should(k8sFixture.NotExistByName()) + Eventually(resources.RoleBinding).Should(k8sFixture.NotExistByName()) + Eventually(resources.ClusterRole).Should(k8sFixture.NotExistByName()) + Eventually(resources.ClusterRoleBinding).Should(k8sFixture.NotExistByName()) + Eventually(resources.PrincipalDeployment).Should(k8sFixture.NotExistByName()) + + for _, serviceName := range resources.ServicesToDelete { + if serviceName == "" { + continue + } + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: resources.PrincipalNamespaceName, + }, + } + Eventually(service).Should(k8sFixture.NotExistByName()) + } + + if fixture.RunningOnOpenShift() { + Eventually(resources.PrincipalRoute).Should(k8sFixture.NotExistByName()) + } +} + +func CreateRequiredSecrets(cfg PrincipalSecretsConfig) { + k8sClient, _ := utils.GetE2ETestKubeClient() + ctx := context.Background() + + By("creating required secrets for principal pod") + + jwtKey := generateJWTSigningKey() + jwtSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.JWTSecretName, + Namespace: cfg.PrincipalNamespaceName, + }, + Data: map[string][]byte{ + "jwt.key": jwtKey, + }, + } + Expect(k8sClient.Create(ctx, jwtSecret)).To(Succeed()) + + caKey, caCert, caCertPEM := generateCertificateAuthority() + caKeyPEM := encodePrivateKeyToPEM(caKey) + + caSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.RootCASecretName, + Namespace: cfg.PrincipalNamespaceName, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "tls.crt": caCertPEM, + "tls.key": caKeyPEM, + "ca.crt": caCertPEM, + }, + } + Expect(k8sClient.Create(ctx, caSecret)).To(Succeed()) + + principalDNS, principalIPs := aggregateSANs(cfg.PrincipalNamespaceName, cfg.PrincipalServiceName, cfg.AdditionalPrincipalSANs) + principalCertPEM, principalKeyPEM := issueCertificate(caCert, caKey, certificateRequest{ + CommonName: cfg.PrincipalServiceName, + DNSNames: principalDNS, + IPAddresses: principalIPs, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }) + createTLSSecret(ctx, k8sClient, cfg.PrincipalNamespaceName, cfg.PrincipalTLSSecretName, principalCertPEM, principalKeyPEM, caCertPEM) + + resourceProxyDNS, resourceProxyIPs := aggregateSANs(cfg.PrincipalNamespaceName, cfg.ResourceProxyServiceName, cfg.AdditionalResourceProxySANs) + resourceProxyCertPEM, resourceProxyKeyPEM := issueCertificate(caCert, caKey, certificateRequest{ + CommonName: cfg.ResourceProxyServiceName, + DNSNames: resourceProxyDNS, + IPAddresses: resourceProxyIPs, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }) + createTLSSecret(ctx, k8sClient, cfg.PrincipalNamespaceName, cfg.ResourceProxyTLSSecretName, resourceProxyCertPEM, resourceProxyKeyPEM, caCertPEM) +} + +func CreateRequiredAgentSecrets(cfg AgentSecretsConfig) { + k8sClient, _ := utils.GetE2ETestKubeClient() + ctx := context.Background() + + agentRootCASecretName := cfg.AgentRootCASecretName + if agentRootCASecretName == "" { + agentRootCASecretName = cfg.PrincipalRootCASecretName + } + + var principalCASecret corev1.Secret + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: cfg.PrincipalRootCASecretName, + Namespace: cfg.PrincipalNamespaceName, + }, &principalCASecret)).To(Succeed()) + + caCertPEM := principalCASecret.Data["tls.crt"] + Expect(caCertPEM).ToNot(BeEmpty(), "CA certificate must be present in principal namespace secret") + caKeyPEM := principalCASecret.Data["tls.key"] + Expect(caKeyPEM).ToNot(BeEmpty(), "CA private key must be present in principal namespace secret") + + caCert := parseCertificate(caCertPEM) + caKey := parsePrivateKey(caKeyPEM) + + clientDNS, clientIPs := aggregateClientSANs(cfg.ClientDNSNames) + clientCertPEM, clientKeyPEM := issueCertificate(caCert, caKey, certificateRequest{ + CommonName: cfg.ClientCommonName, + DNSNames: clientDNS, + IPAddresses: clientIPs, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + }) + + createTLSSecret(ctx, k8sClient, cfg.AgentNamespace.Name, cfg.ClientTLSSecretName, clientCertPEM, clientKeyPEM, caCertPEM) + + // Propagate CA certificate without private key to the agent namespace + propagatedCASecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: agentRootCASecretName, + Namespace: cfg.AgentNamespace.Name, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "tls.crt": caCertPEM, + "ca.crt": caCertPEM, + }, + } + Expect(k8sClient.Create(ctx, propagatedCASecret)).To(Succeed()) +} + +func CreateClusterRegistrationSecret(cfg ClusterRegistrationSecretConfig) { + k8sClient, _ := utils.GetE2ETestKubeClient() + ctx := context.Background() + + server := cfg.Server + if server == "" { + port := cfg.ResourceProxyPort + if port == 0 { + port = 9090 + } + host := fmt.Sprintf("%s.%s.svc", cfg.ResourceProxyServiceName, cfg.PrincipalNamespaceName) + server = fmt.Sprintf("https://%s:%d?agentName=%s", host, port, cfg.AgentName) + } + + var caSecret corev1.Secret + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: cfg.PrincipalRootCASecretName, + Namespace: cfg.PrincipalNamespaceName, + }, &caSecret)).To(Succeed()) + + caData := caSecret.Data["ca.crt"] + if len(caData) == 0 { + caData = caSecret.Data["tls.crt"] + } + Expect(caData).ToNot(BeEmpty(), "CA certificate missing from principal root secret") + + var agentTLSSecret corev1.Secret + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: cfg.AgentTLSSecretName, + Namespace: cfg.AgentNamespaceName, + }, &agentTLSSecret)).To(Succeed()) + + clientCert := agentTLSSecret.Data["tls.crt"] + Expect(clientCert).ToNot(BeEmpty(), "agent TLS certificate missing") + clientKey := agentTLSSecret.Data["tls.key"] + Expect(clientKey).ToNot(BeEmpty(), "agent TLS private key missing") + + configPayload, err := json.Marshal(map[string]any{ + "tlsClientConfig": map[string]any{ + "insecure": false, + "certData": base64.StdEncoding.EncodeToString(clientCert), + "keyData": base64.StdEncoding.EncodeToString(clientKey), + "caData": base64.StdEncoding.EncodeToString(caData), + }, + }) + Expect(err).ToNot(HaveOccurred()) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("cluster-%s", cfg.AgentName), + Namespace: cfg.PrincipalNamespaceName, + Labels: map[string]string{ + common.ArgoCDSecretTypeLabel: "cluster", + "argocd-agent.argoproj-labs.io/agent-name": cfg.AgentName, + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "name": []byte(cfg.AgentName), + "server": []byte(server), + "config": configPayload, + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) +} + +func VerifyExpectedResourcesExist(params VerifyExpectedResourcesExistParams) { + shouldExpectRoute := true + if params.ExpectRoute != nil { + shouldExpectRoute = *params.ExpectRoute + } + + By("verifying expected resources exist") + + if params.SecretNames.RedisInitialPasswordSecretName != "" { + Eventually(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: params.SecretNames.RedisInitialPasswordSecretName, + Namespace: params.Namespace.Name, + }, + }, "60s", "2s").Should(k8sFixture.ExistByName()) + } + + Eventually(params.ServiceAccount).Should(k8sFixture.ExistByName()) + Eventually(params.Role).Should(k8sFixture.ExistByName()) + Eventually(params.RoleBinding).Should(k8sFixture.ExistByName()) + Eventually(params.ClusterRole).Should(k8sFixture.ExistByName()) + Eventually(params.ClusterRoleBinding).Should(k8sFixture.ExistByName()) + + for _, serviceName := range params.ServiceNames { + if serviceName == "" { + continue + } + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: params.Namespace.Name, + }, + } + Eventually(service).Should(k8sFixture.ExistByName(), "Service '%s' should exist in namespace '%s'", serviceName, params.Namespace.Name) + + if serviceName != params.ArgoCDAgentPrincipalName { + Expect(string(service.Spec.Type)).To(Equal("ClusterIP"), "Service '%s' should have ClusterIP type", serviceName) + } + } + + if shouldExpectRoute && fixture.RunningOnOpenShift() { + Eventually(params.PrincipalRoute).Should(k8sFixture.ExistByName()) + } + + for _, deploymentName := range params.DeploymentNames { + if deploymentName == "" { + continue + } + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: params.Namespace.Name, + }, + } + Eventually(depl).Should(k8sFixture.ExistByName(), "Deployment '%s' should exist in namespace '%s'", deploymentName, params.Namespace.Name) + } + + Eventually(params.PrincipalDeployment).Should(k8sFixture.ExistByName()) + Eventually(params.PrincipalDeployment).Should(k8sFixture.HaveLabelWithValue("app.kubernetes.io/managed-by", params.ArgoCDName)) + Eventually(params.PrincipalDeployment).Should(k8sFixture.HaveLabelWithValue("app.kubernetes.io/name", params.ArgoCDAgentPrincipalName)) + Eventually(params.PrincipalDeployment).Should(k8sFixture.HaveLabelWithValue("app.kubernetes.io/part-of", "argocd-agent")) +} + +func VerifyLogs(deploymentName, namespace string, requiredMessages []string) { + Eventually(func() bool { + logOutput, err := osFixture.ExecCommandWithOutputParam(false, "true", "kubectl", "logs", + "deployment/"+deploymentName, "-n", namespace, "--tail=200") + if err != nil { + GinkgoWriter.Println("Error getting agent logs: ", err) + return false + } + + for _, msg := range requiredMessages { + if !strings.Contains(logOutput, msg) { + GinkgoWriter.Println("Expected agent log not found:", msg) + return false + } + } + return true + }, "120s", "5s").Should(BeTrue(), "Agent should process cluster cache updates") +} + +type certificateRequest struct { + CommonName string + DNSNames []string + IPAddresses []net.IP + ExtKeyUsage []x509.ExtKeyUsage +} + +func generateCertificateAuthority() (*rsa.PrivateKey, *x509.Certificate, []byte) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).ToNot(HaveOccurred()) + + template := x509.Certificate{ + SerialNumber: randomSerialNumber(), + Subject: pkix.Name{CommonName: "argocd-agent-ca"}, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + Expect(err).ToNot(HaveOccurred()) + + cert, err := x509.ParseCertificate(certDER) + Expect(err).ToNot(HaveOccurred()) + + return privateKey, cert, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) +} + +func issueCertificate(caCert *x509.Certificate, caKey *rsa.PrivateKey, req certificateRequest) ([]byte, []byte) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).ToNot(HaveOccurred()) + + template := x509.Certificate{ + SerialNumber: randomSerialNumber(), + Subject: pkix.Name{ + CommonName: req.CommonName, + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: req.ExtKeyUsage, + DNSNames: req.DNSNames, + IPAddresses: req.IPAddresses, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, caCert, &key.PublicKey, caKey) + Expect(err).ToNot(HaveOccurred()) + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM := encodePrivateKeyToPEM(key) + + return certPEM, keyPEM +} + +func createTLSSecret(ctx context.Context, k8sClient client.Client, namespace, secretName string, certPEM, keyPEM, caCertPEM []byte) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "tls.crt": certPEM, + "tls.key": keyPEM, + "ca.crt": caCertPEM, + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) +} + +func generateJWTSigningKey() []byte { + key, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).ToNot(HaveOccurred()) + + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + Expect(err).ToNot(HaveOccurred()) + + return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) +} + +func encodePrivateKeyToPEM(key *rsa.PrivateKey) []byte { + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + Expect(err).ToNot(HaveOccurred()) + + return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) +} + +func parseCertificate(certPEM []byte) *x509.Certificate { + block, _ := pem.Decode(certPEM) + Expect(block).ToNot(BeNil(), "invalid certificate data") + cert, err := x509.ParseCertificate(block.Bytes) + Expect(err).ToNot(HaveOccurred()) + return cert +} + +func parsePrivateKey(keyPEM []byte) *rsa.PrivateKey { + block, _ := pem.Decode(keyPEM) + Expect(block).ToNot(BeNil(), "invalid private key data") + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + Expect(err).ToNot(HaveOccurred()) + + privateKey, ok := parsedKey.(*rsa.PrivateKey) + Expect(ok).To(BeTrue(), "private key is not RSA") + return privateKey +} + +func randomSerialNumber() *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + Expect(err).ToNot(HaveOccurred()) + return serialNumber +} + +func aggregateSANs(namespace, serviceName string, additional []string) ([]string, []net.IP) { + defaults := buildDefaultSANs(serviceName, namespace) + return aggregateSANLists(defaults, additional) +} + +func aggregateClientSANs(additional []string) ([]string, []net.IP) { + return aggregateSANLists(nil, additional) +} + +func aggregateSANLists(defaults, additional []string) ([]string, []net.IP) { + dnsSet := map[string]struct{}{} + ipSet := map[string]struct{}{} + var dnsNames []string + var ipAddresses []net.IP + + addEntry := func(entry string) { + entry = strings.TrimSpace(entry) + if entry == "" { + return + } + if ip := net.ParseIP(entry); ip != nil { + key := ip.String() + if _, found := ipSet[key]; !found { + ipSet[key] = struct{}{} + ipAddresses = append(ipAddresses, ip) + } + return + } + if _, found := dnsSet[entry]; !found { + dnsSet[entry] = struct{}{} + dnsNames = append(dnsNames, entry) + } + } + + for _, entry := range defaults { + addEntry(entry) + } + for _, entry := range additional { + addEntry(entry) + } + + sort.Strings(dnsNames) + sort.Slice(ipAddresses, func(i, j int) bool { + return bytes.Compare(ipAddresses[i], ipAddresses[j]) < 0 + }) + + return dnsNames, ipAddresses +} + +func buildDefaultSANs(serviceName, namespace string) []string { + if serviceName == "" || namespace == "" { + return nil + } + return []string{ + serviceName, + fmt.Sprintf("%s.%s", serviceName, namespace), + fmt.Sprintf("%s.%s.svc", serviceName, namespace), + fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace), + } +} diff --git a/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership.go b/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership.go new file mode 100644 index 00000000000..4a7ce74e63f --- /dev/null +++ b/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership.go @@ -0,0 +1,189 @@ +/* +Copyright 2025. + +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 sequential + +import ( + "context" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-125_validate_role_ownership", func() { + + var ( + ctx context.Context + k8sClient client.Client + ) + const ( + applicationControllerClusterRoleName = "openshift-gitops-openshift-gitops-argocd-application-controller" + applicationSetControllerClusterRoleName = "openshift-gitops-openshift-gitops-argocd-applicationset-controller" + serverClusterRoleName = "openshift-gitops-openshift-gitops-argocd-server" + applicationControllerClusterRoleBindingName = "openshift-gitops-openshift-gitops-argocd-application-controller" + applicationSetControllerClusterRoleBindingName = "openshift-gitops-openshift-gitops-argocd-applicationset-controller" + serverClusterRoleBindingName = "openshift-gitops-openshift-gitops-argocd-server" + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + It("validates that the role bug is fixed", func() { + + By("checking that the default ClusterRole and clusterroleBinding for the ArgoCD Application Controller and Server exists") + defaultControllerClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationControllerClusterRoleName, + }, + } + defaultApplicationSetControllerClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationSetControllerClusterRoleName, + }, + } + defaultServerClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: serverClusterRoleName, + }, + } + defaultControllerClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationControllerClusterRoleBindingName, + }, + } + defaultApplicationSetControllerClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationSetControllerClusterRoleBindingName, + }, + } + defaultServerClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: serverClusterRoleBindingName, + }, + } + Eventually(defaultControllerClusterRole).Should(k8sFixture.ExistByName()) + Eventually(defaultApplicationSetControllerClusterRole).Should(k8sFixture.ExistByName()) + Eventually(defaultServerClusterRole).Should(k8sFixture.ExistByName()) + Eventually(defaultControllerClusterRoleBinding).Should(k8sFixture.ExistByName()) + Eventually(defaultApplicationSetControllerClusterRoleBinding).Should(k8sFixture.ExistByName()) + Eventually(defaultServerClusterRoleBinding).Should(k8sFixture.ExistByName()) + + By("fetching initial UID of the clusterrole") + initialControllerUid := defaultControllerClusterRole.GetUID() + initialApplicationSetControllerUid := defaultApplicationSetControllerClusterRole.GetUID() + initialServerUid := defaultServerClusterRole.GetUID() + initialControllerRoleBindingUid := defaultControllerClusterRoleBinding.GetUID() + initialApplicationSetControllerRoleBindingUid := defaultApplicationSetControllerClusterRoleBinding.GetUID() + initialServerRoleBindingUid := defaultServerClusterRoleBinding.GetUID() + + defaultArgocd := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-gitops", + Namespace: "openshift-gitops", + }, + } + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(defaultArgocd, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("creating new ArgoCD instance to trigger the check") + ns, nsCleanup := fixture.CreateNamespaceWithCleanupFunc("gitops") + defer nsCleanup() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-gitops-openshift", + Namespace: ns.Name, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("checking that the default ClusterRole for the ArgoCD Application Controller still exists") + newControllerClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationControllerClusterRoleName, + }, + } + newApplicationSetControllerClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationSetControllerClusterRoleName, + }, + } + newServerClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: serverClusterRoleName, + }, + } + newControllerClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationControllerClusterRoleBindingName, + }, + } + newApplicationSetControllerClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationSetControllerClusterRoleBindingName, + }, + } + newServerClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: serverClusterRoleBindingName, + }, + } + + Eventually(newControllerClusterRole).Should(k8sFixture.ExistByName()) + Eventually(newApplicationSetControllerClusterRole).Should(k8sFixture.ExistByName()) + Eventually(newServerClusterRole).Should(k8sFixture.ExistByName()) + + Eventually(newControllerClusterRoleBinding).Should(k8sFixture.ExistByName()) + Eventually(newApplicationSetControllerClusterRoleBinding).Should(k8sFixture.ExistByName()) + Eventually(newServerClusterRoleBinding).Should(k8sFixture.ExistByName()) + + By("fetching UID of the clusterrole after reconciliation") + afterControllerReconcileUid := newControllerClusterRole.GetUID() + afterApplicationSetControllerReconcileUid := newApplicationSetControllerClusterRole.GetUID() + afterServerReconcileUid := newServerClusterRole.GetUID() + + afterControllerRoleBindingReconcileUid := newControllerClusterRoleBinding.GetUID() + afterApplicationSetControllerRoleBindingReconcileUid := newApplicationSetControllerClusterRoleBinding.GetUID() + afterServerRoleBindingReconcileUid := newServerClusterRoleBinding.GetUID() + + By("comparing the UID to check if the ClusterRole was recreated") + Expect(initialControllerUid).To(Equal(afterControllerReconcileUid), "the ClusterRole was recreated") + Expect(initialApplicationSetControllerUid).To(Equal(afterApplicationSetControllerReconcileUid), "the ClusterRole was recreated") + Expect(initialServerUid).To(Equal(afterServerReconcileUid), "the ClusterRole was recreated") + + Expect(initialControllerRoleBindingUid).To(Equal(afterControllerRoleBindingReconcileUid), "the ClusterRoleBinding was recreated") + Expect(initialApplicationSetControllerRoleBindingUid).To(Equal(afterApplicationSetControllerRoleBindingReconcileUid), "the ClusterRoleBinding was recreated") + Expect(initialServerRoleBindingUid).To(Equal(afterServerRoleBindingReconcileUid), "the ClusterRoleBinding was recreated") + + }) + + }) +}) diff --git a/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership_agent_agent_test.go b/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership_agent_agent_test.go new file mode 100644 index 00000000000..dfbf48dfcab --- /dev/null +++ b/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership_agent_agent_test.go @@ -0,0 +1,249 @@ +/* +Copyright 2025. + +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 sequential + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" +) + +var _ = Describe("GitOps Operator Sequential E2E Tests", func() { + + const ( + clusterRoleName = "argocd-agent-argocd-agent-agent-agent" + clusterRoleBindingName = "argocd-agent-argocd-agent-agent-agent" + + //Secret Names + agentRootCASecretName = "argocd-agent-ca" + agentClientTLSSecretName = "argocd-agent-client-tls" + ) + + Context("1-125_validate_role_ownership_agent_agent", func() { + var ( + k8sClient client.Client + ctx context.Context + argoCD *argov1beta1api.ArgoCD + ns *corev1.Namespace + cleanupFunc func() + serviceNames []string + agentDeployment *appsv1.Deployment + ) + + BeforeEach(func() { + fixture.EnsureSequentialCleanSlate() + fixture.SetEnvInOperatorSubscriptionOrDeployment("ARGOCD_CLUSTER_CONFIG_NAMESPACES", "openshift-gitops, argocd-agent") + + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + ns, cleanupFunc = fixture.CreateNamespaceWithCleanupFunc("argocd-agent") + + // Define ArgoCD CR with agent enabled + argoCD = &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-agent", + Namespace: ns.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Enabled: ptr.To(false), + }, + Server: argov1beta1api.ArgoCDServerSpec{ + Enabled: ptr.To(false), + }, + ArgoCDAgent: &argov1beta1api.ArgoCDAgentSpec{ + Agent: &argov1beta1api.AgentSpec{ + Enabled: ptr.To(true), + Creds: "mtls:any", + LogLevel: "info", + LogFormat: "text", + Client: &argov1beta1api.AgentClientSpec{ + PrincipalServerAddress: "argocd-agent-principal.example.com", + PrincipalServerPort: "443", + EnableWebSocket: ptr.To(false), + EnableCompression: ptr.To(false), + KeepAliveInterval: "30s", + }, + TLS: &argov1beta1api.AgentTLSSpec{ + SecretName: agentClientTLSSecretName, + RootCASecretName: agentRootCASecretName, + Insecure: ptr.To(false), + }, + Redis: &argov1beta1api.AgentRedisSpec{ + ServerAddress: fmt.Sprintf("%s-%s:%d", "argocd-agent", "redis", common.ArgoCDDefaultRedisPort), + }, + }, + }, + }, + } + + serviceNames = []string{ + "argocd-agent-agent-agent-metrics", + "argocd-agent-agent-agent-healthz", + "argocd-agent-redis", + } + + agentDeployment = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-agent-agent-agent", + Namespace: ns.Name, + }, + } + + }) + + AfterEach(func() { + By("Cleanup namespace") + if cleanupFunc != nil { + cleanupFunc() + } + }) + + verifyExpectedResourcesExist := func(ns *corev1.Namespace) { + + By("verifying expected resources exist") + for _, serviceName := range serviceNames { + + By("verifying Service '" + serviceName + "' exists and is a LoadBalancer or ClusterIP depending on which service") + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: ns.Name, + }, + } + Eventually(service).Should(k8sFixture.ExistByName()) + Expect(string(service.Spec.Type)).To(Equal("ClusterIP")) + + } + By("verifying primary agent Deployment has expected values") + + Eventually(agentDeployment).Should(k8sFixture.ExistByName()) + Eventually(agentDeployment).Should(k8sFixture.HaveLabelWithValue("app.kubernetes.io/managed-by", "argocd-agent")) + Eventually(agentDeployment).Should(k8sFixture.HaveLabelWithValue("app.kubernetes.io/name", "argocd-agent-agent-agent")) + Eventually(agentDeployment).Should(k8sFixture.HaveLabelWithValue("app.kubernetes.io/part-of", "argocd-agent")) + + } + + It("check agent clusterRole and clusterRoleBinding", func() { + + By("creating ArgoCD instance with agent enabled") + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("verifying expected resources are created with correct values") + verifyExpectedResourcesExist(ns) + + By("verifying ClusterRole and ClusterRoleBinding for agent exist with correct names") + + clusterRole := &rbacv1.ClusterRole{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleName}, clusterRole) + }).Should(Succeed()) + initialClusterRoleUid := clusterRole.GetUID() + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleBindingName}, clusterRoleBinding) + }).Should(Succeed()) + initialClusterRoleBindingUid := clusterRoleBinding.GetUID() + + By("Create namespace-scoped ArgoCD instance namespace") + nsScoped, cleanupFuncScoped := fixture.CreateNamespaceWithCleanupFunc("agent") + defer cleanupFuncScoped() + + argoCD1 := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-agent-argocd", + Namespace: nsScoped.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Enabled: ptr.To(false), + }, + Server: argov1beta1api.ArgoCDServerSpec{ + Enabled: ptr.To(false), + }, + ArgoCDAgent: &argov1beta1api.ArgoCDAgentSpec{ + Agent: &argov1beta1api.AgentSpec{ + Enabled: ptr.To(true), + Creds: "mtls:any", + LogLevel: "info", + LogFormat: "text", + Client: &argov1beta1api.AgentClientSpec{ + PrincipalServerAddress: "argocd-agent-principal.example.com", + PrincipalServerPort: "443", + EnableWebSocket: ptr.To(false), + EnableCompression: ptr.To(false), + KeepAliveInterval: "30s", + }, + TLS: &argov1beta1api.AgentTLSSpec{ + SecretName: agentClientTLSSecretName, + RootCASecretName: agentRootCASecretName, + Insecure: ptr.To(false), + }, + Redis: &argov1beta1api.AgentRedisSpec{ + ServerAddress: fmt.Sprintf("%s-%s:%d", "argocd-agent-argocd", "redis", common.ArgoCDDefaultRedisPort), + }, + }, + }, + }, + } + + By("Create namespace-scoped ArgoCD instance with agent") + + Expect(k8sClient.Create(ctx, argoCD1)).To(Succeed()) + Eventually(argoCD1, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("Verify UID of ClusterRole and ClusterRoleBinding") + newClusterRole := &rbacv1.ClusterRole{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleName}, newClusterRole) + }).Should(Succeed(), "ClusterRole should exist and be fetchable") + newClusterRoleUid := newClusterRole.GetUID() + + newClusterRoleBinding := &rbacv1.ClusterRoleBinding{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleBindingName}, newClusterRoleBinding) + }).Should(Succeed(), "ClusterRoleBinding should exist and be fetchable") + newClusterRoleBindingUid := newClusterRoleBinding.GetUID() + + Expect(newClusterRoleUid).To(Equal(initialClusterRoleUid), "ClusterRole UID should remain the same after creating namespace-scoped ArgoCD instance") + Expect(newClusterRoleBindingUid).To(Equal(initialClusterRoleBindingUid), "ClusterRoleBinding UID should remain the same after creating namespace-scoped ArgoCD instance") + + }) + + }) + +}) diff --git a/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership_agent_principal_test.go b/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership_agent_principal_test.go new file mode 100644 index 00000000000..d096088025e --- /dev/null +++ b/test/openshift/e2e/ginkgo/sequential/1-125_validate_role_ownership_agent_principal_test.go @@ -0,0 +1,320 @@ +/* +Copyright 2025. + +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 sequential + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + agentFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/agent" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + deploymentFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/deployment" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" +) + +var _ = Describe("GitOps Operator Sequential E2E Tests", func() { + + Context("1-125_validate_role_ownership_agent_principal", func() { + + var ( + k8sClient client.Client + ctx context.Context + argoCD *argov1beta1api.ArgoCD + ns *corev1.Namespace + cleanupFunc func() + serviceAccount *corev1.ServiceAccount + role *rbacv1.Role + roleBinding *rbacv1.RoleBinding + clusterRole *rbacv1.ClusterRole + clusterRoleBinding *rbacv1.ClusterRoleBinding + serviceNames []string + deploymentNames []string + principalDeployment *appsv1.Deployment + secretNames agentFixture.AgentSecretNames + principalRoute *routev1.Route + resourceProxyServiceName string + ) + const ( + argoCDName = "argocd-principal" + argoCDAgentPrincipalName = "argocd-principal-agent-principal" // argoCDName + "-agent-principal" + principalMetricsServiceFmt = "%s-agent-principal-metrics" + principalRedisProxyServiceFmt = "%s-agent-principal-redisproxy" + principalHealthzServiceFmt = "%s-agent-principal-healthz" + clusterRoleName = "argocd-principal-argocd-principal-agent-principal" + clusterRoleBindingName = "argocd-principal-argocd-principal-agent-principal" + nsScopedArgoCDName = "argocd-principal-argocd" + + //Secret Names + agentJWTSecretName = "argocd-agent-jwt" + agentPrincipalTLSSecretName = "argocd-agent-principal-tls" + agentRootCASecretName = "argocd-agent-ca" + agentResourceProxyTLSSecretName = "argocd-agent-resource-proxy-tls" + ) + + BeforeEach(func() { + fixture.EnsureSequentialCleanSlate() + fixture.SetEnvInOperatorSubscriptionOrDeployment("ARGOCD_CLUSTER_CONFIG_NAMESPACES", "openshift-gitops, argocd-principal") + + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + ns, cleanupFunc = fixture.CreateNamespaceWithCleanupFunc("argocd-principal") + + // Define ArgoCD CR with principal enabled + argoCD = &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDName, + Namespace: ns.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Enabled: ptr.To(false), + }, + ArgoCDAgent: &argov1beta1api.ArgoCDAgentSpec{ + Principal: &argov1beta1api.PrincipalSpec{ + Enabled: ptr.To(true), + Auth: "mtls:CN=([^,]+)", + LogLevel: "info", + Namespace: &argov1beta1api.PrincipalNamespaceSpec{ + AllowedNamespaces: []string{ + "*", + }, + }, + TLS: &argov1beta1api.PrincipalTLSSpec{ + InsecureGenerate: ptr.To(true), + }, + JWT: &argov1beta1api.PrincipalJWTSpec{ + InsecureGenerate: ptr.To(true), + }, + Server: &argov1beta1api.PrincipalServerSpec{ + KeepAliveMinInterval: "30s", + }, + }, + }, + SourceNamespaces: []string{ + "agent-managed", + "agent-autonomous", + }, + }, + } + + // Define required resources for principal pod + serviceAccount = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDAgentPrincipalName, + Namespace: ns.Name, + }, + } + + role = &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDAgentPrincipalName, + Namespace: ns.Name, + }, + } + + roleBinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDAgentPrincipalName, + Namespace: ns.Name, + }, + } + + clusterRole = &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterRoleName, + }, + } + + clusterRoleBinding = &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterRoleBindingName, + }, + } + + secretNames = agentFixture.AgentSecretNames{ + JWTSecretName: agentJWTSecretName, + PrincipalTLSSecretName: agentPrincipalTLSSecretName, + RootCASecretName: agentRootCASecretName, + ResourceProxyTLSSecretName: agentResourceProxyTLSSecretName, + RedisInitialPasswordSecretName: "argocd-principal-redis-initial-password", + } + + resourceProxyServiceName = fmt.Sprintf("%s-agent-principal-resource-proxy", argoCDName) + serviceNames = []string{ + argoCDAgentPrincipalName, + fmt.Sprintf(principalMetricsServiceFmt, argoCDName), + fmt.Sprintf("%s-redis", argoCDName), + fmt.Sprintf("%s-repo-server", argoCDName), + fmt.Sprintf("%s-server", argoCDName), + fmt.Sprintf(principalRedisProxyServiceFmt, argoCDName), + resourceProxyServiceName, + fmt.Sprintf(principalHealthzServiceFmt, argoCDName), + } + deploymentNames = []string{fmt.Sprintf("%s-redis", argoCDName), fmt.Sprintf("%s-repo-server", argoCDName), fmt.Sprintf("%s-server", argoCDName)} + + principalDeployment = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDAgentPrincipalName, + Namespace: ns.Name, + }, + } + + principalRoute = &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-agent-principal", argoCDName), + Namespace: ns.Name, + }, + } + }) + + AfterEach(func() { + By("Cleanup cluster-scoped resources") + if clusterRole != nil { + _ = k8sClient.Delete(ctx, clusterRole) + } + if clusterRoleBinding != nil { + _ = k8sClient.Delete(ctx, clusterRoleBinding) + } + + By("Cleanup namespace") + if cleanupFunc != nil { + cleanupFunc() + } + }) + + createRequiredSecrets := func(namespace *corev1.Namespace, additionalPrincipalSANs ...string) { + agentFixture.CreateRequiredSecrets(agentFixture.PrincipalSecretsConfig{ + PrincipalNamespaceName: namespace.Name, + PrincipalServiceName: argoCDAgentPrincipalName, + ResourceProxyServiceName: resourceProxyServiceName, + JWTSecretName: secretNames.JWTSecretName, + PrincipalTLSSecretName: secretNames.PrincipalTLSSecretName, + RootCASecretName: secretNames.RootCASecretName, + ResourceProxyTLSSecretName: secretNames.ResourceProxyTLSSecretName, + AdditionalPrincipalSANs: additionalPrincipalSANs, + }) + } + + verifyExpectedResourcesExist := func(namespace *corev1.Namespace, expectRoute ...bool) { + var expectRoutePtr *bool + if len(expectRoute) > 0 { + expectRoutePtr = ptr.To(expectRoute[0]) + } + + agentFixture.VerifyExpectedResourcesExist(agentFixture.VerifyExpectedResourcesExistParams{ + Namespace: namespace, + ArgoCDAgentPrincipalName: argoCDAgentPrincipalName, + ArgoCDName: argoCDName, + ServiceAccount: serviceAccount, + Role: role, + RoleBinding: roleBinding, + ClusterRole: clusterRole, + ClusterRoleBinding: clusterRoleBinding, + PrincipalDeployment: principalDeployment, + PrincipalRoute: principalRoute, + SecretNames: secretNames, + ServiceNames: serviceNames, + DeploymentNames: deploymentNames, + ExpectRoute: expectRoutePtr, + }) + } + + It("validates that the role bug is fixed in agent", func() { + By("Create ArgoCD instance") + + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("Verify expected resources are created for principal pod") + + verifyExpectedResourcesExist(ns) + + By("Create required secrets and certificates for principal pod to start properly") + + createRequiredSecrets(ns) + + By("verify that deployment is in Ready state") + + Eventually(principalDeployment, "120s", "5s").Should(deploymentFixture.HaveReadyReplicas(1), "Principal deployment should become ready") + + By("Fetch Uid of clusterrole and clusterrolebinding") + clusterRole = &rbacv1.ClusterRole{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleName}, clusterRole) + }).Should(Succeed(), "ClusterRole should exist") + initialClusterRoleUid := clusterRole.GetUID() + + clusterRoleBinding = &rbacv1.ClusterRoleBinding{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleBindingName}, clusterRoleBinding) + }).Should(Succeed(), "ClusterRoleBinding should exist") + initialClusterRoleBindingUid := clusterRoleBinding.GetUID() + + By("Create namespace-scoped ArgoCD instance namespace") + + // Create namespace for hosting namespace-scoped ArgoCD instance with principal + nsScoped, cleanupFuncScoped := fixture.CreateNamespaceWithCleanupFunc("principal") + defer cleanupFuncScoped() + + // Update namespace in ArgoCD CR + argoCD.ResourceVersion = "" + argoCD.UID = "" + argoCD.Name = "argocd-principal-argocd" + argoCD.Namespace = nsScoped.Name + + // Update namespace in resource references + serviceAccount.Namespace = nsScoped.Name + role.Namespace = nsScoped.Name + roleBinding.Namespace = nsScoped.Name + principalDeployment.Namespace = nsScoped.Name + principalRoute.Namespace = nsScoped.Name + + By("Create namespace-scoped ArgoCD instance with principal") + + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("Verify UID of ClusterRole and ClusterRoleBinding") + newClusterRole := &rbacv1.ClusterRole{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleName}, newClusterRole) + }).Should(Succeed(), "ClusterRole should exist and be fetchable") + newClusterRoleUid := newClusterRole.GetUID() + + newClusterRoleBinding := &rbacv1.ClusterRoleBinding{} + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleBindingName}, newClusterRoleBinding) + }).Should(Succeed(), "ClusterRoleBinding should exist and be fetchable") + newClusterRoleBindingUid := newClusterRoleBinding.GetUID() + + Expect(newClusterRoleUid).To(Equal(initialClusterRoleUid), "ClusterRole UID should remain the same after creating namespace-scoped ArgoCD instance") + Expect(newClusterRoleBindingUid).To(Equal(initialClusterRoleBindingUid), "ClusterRoleBinding UID should remain the same after creating namespace-scoped ArgoCD instance") + + }) + + }) +})