diff --git a/bundle/manifests/gitops-operator.clusterserviceversion.yaml b/bundle/manifests/gitops-operator.clusterserviceversion.yaml index fd4cfa2a1..2163f06df 100644 --- a/bundle/manifests/gitops-operator.clusterserviceversion.yaml +++ b/bundle/manifests/gitops-operator.clusterserviceversion.yaml @@ -574,6 +574,7 @@ spec: - apiGroups: - config.openshift.io resources: + - authentications - clusterversions - ingresses verbs: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ef9fb3c82..40d7350a8 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -223,6 +223,7 @@ rules: - apiGroups: - config.openshift.io resources: + - authentications - clusterversions - ingresses verbs: diff --git a/controllers/argocd/argocd.go b/controllers/argocd/argocd.go index e7928dd79..ba7b1b41e 100644 --- a/controllers/argocd/argocd.go +++ b/controllers/argocd/argocd.go @@ -17,11 +17,14 @@ limitations under the License. package argocd import ( + "context" + argoapp "github.com/argoproj-labs/argocd-operator/api/v1beta1" + argoappController "github.com/argoproj-labs/argocd-operator/controllers/argocd" v1 "k8s.io/api/core/v1" resourcev1 "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" ) @@ -86,7 +89,10 @@ func getArgoDexSpec() *argoapp.ArgoCDDexSpec { } } -func getArgoSSOSpec() *argoapp.ArgoCDSSOSpec { +func getArgoSSOSpec(client client.Client) *argoapp.ArgoCDSSOSpec { + if argoappController.IsOpenShiftCluster() && argoappController.IsExternalAuthenticationEnabledOnCluster(context.TODO(), client) { + return nil + } return &argoapp.ArgoCDSSOSpec{ Provider: argoapp.SSOProviderTypeDex, Dex: getArgoDexSpec(), @@ -180,7 +186,7 @@ func getDefaultRBAC() argoapp.ArgoCDRBACSpec { // NewCR returns an ArgoCD reference optimized for use in OpenShift // with comprehensive default resource exclusions -func NewCR(name, ns string) (*argoapp.ArgoCD, error) { +func NewCR(name, ns string, client client.Client) (*argoapp.ArgoCD, error) { b, err := yaml.Marshal([]resource{ { APIGroups: []string{"", "discovery.k8s.io"}, @@ -239,7 +245,7 @@ func NewCR(name, ns string) (*argoapp.ArgoCD, error) { Spec: argoapp.ArgoCDSpec{ ApplicationSet: getArgoApplicationSetSpec(), Controller: getArgoControllerSpec(), - SSO: getArgoSSOSpec(), + SSO: getArgoSSOSpec(client), Grafana: getArgoGrafanaSpec(), HA: getArgoHASpec(), Redis: getArgoRedisSpec(), diff --git a/controllers/argocd/argocd_test.go b/controllers/argocd/argocd_test.go index 7435a8485..0132c53f6 100644 --- a/controllers/argocd/argocd_test.go +++ b/controllers/argocd/argocd_test.go @@ -21,13 +21,22 @@ import ( "testing" argoapp "github.com/argoproj-labs/argocd-operator/api/v1beta1" + configv1 "github.com/openshift/api/config/v1" "gotest.tools/assert" v1 "k8s.io/api/core/v1" resourcev1 "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestArgoCD(t *testing.T) { - testArgoCD, _ := NewCR("openshift-gitops", "openshift-gitops") + scheme := runtime.NewScheme() + _ = argoapp.AddToScheme(scheme) + _ = configv1.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + testArgoCD, _ := NewCR("openshift-gitops", "openshift-gitops", fakeClient) testApplicationSetResources := &v1.ResourceRequirements{ Requests: v1.ResourceList{ @@ -190,7 +199,15 @@ func TestArgoCD(t *testing.T) { } func TestDexConfiguration(t *testing.T) { - testArgoCD, _ := NewCR("openshift-gitops", "openshift-gitops") + scheme := runtime.NewScheme() + _ = argoapp.AddToScheme(scheme) + _ = configv1.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + Build() + + testArgoCD, _ := NewCR("openshift-gitops", "openshift-gitops", fakeClient) // Verify Dex OpenShift Configuration assert.Equal(t, testArgoCD.Spec.SSO.Dex.OpenShiftOAuth, true) diff --git a/controllers/gitopsservice_controller.go b/controllers/gitopsservice_controller.go index 5bb7ffc6c..4782e40e8 100644 --- a/controllers/gitopsservice_controller.go +++ b/controllers/gitopsservice_controller.go @@ -134,6 +134,7 @@ type ReconcileGitopsService struct { DisableDefaultInstall bool } +// +kubebuilder:rbac:groups=config.openshift.io,resources=authentications,verbs=get;list;watch //+kubebuilder:rbac:groups=pipelines.openshift.io,resources=gitopsservices,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=pipelines.openshift.io,resources=gitopsservices/status,verbs=get;update;patch //+kubebuilder:rbac:groups=pipelines.openshift.io,resources=gitopsservices/finalizers,verbs=update @@ -313,7 +314,7 @@ func (r *ReconcileGitopsService) Reconcile(ctx context.Context, request reconcil func (r *ReconcileGitopsService) ensureDefaultArgoCDInstanceDoesntExist() error { - defaultArgoCDInstance, err := argocd.NewCR(common.ArgoCDInstanceName, serviceNamespace) + defaultArgoCDInstance, err := argocd.NewCR(common.ArgoCDInstanceName, serviceNamespace, r.Client) if err != nil { return err } @@ -349,7 +350,7 @@ func (r *ReconcileGitopsService) ensureDefaultArgoCDInstanceDoesntExist() error func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipelinesv1alpha1.GitopsService, reqLogger logr.Logger) (reconcile.Result, error) { - defaultArgoCDInstance, err := argocd.NewCR(common.ArgoCDInstanceName, serviceNamespace) + defaultArgoCDInstance, err := argocd.NewCR(common.ArgoCDInstanceName, serviceNamespace, r.Client) if err != nil { return reconcile.Result{}, err } diff --git a/go.mod b/go.mod index 507bbdb5c..fb03f24ec 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.5 require ( github.com/argoproj-labs/argo-rollouts-manager v0.0.8-0.20260218104514-432c01ce417a - github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20260211145236-4c05ef8fa3d7 + github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20260225073619-a52ee52d3941 github.com/argoproj/argo-cd/v3 v3.3.0 github.com/argoproj/gitops-engine v0.7.1-0.20251217140045-5baed5604d2d github.com/go-logr/logr v1.4.3 @@ -43,7 +43,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect - github.com/argoproj-labs/argocd-image-updater v1.1.0 // indirect + github.com/argoproj-labs/argocd-image-updater v1.1.1 // indirect github.com/argoproj/pkg v0.13.7-0.20250305113207-cbc37dc61de5 // indirect github.com/argoproj/pkg/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 2c068443c..f19a36392 100644 --- a/go.sum +++ b/go.sum @@ -37,10 +37,10 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/argoproj-labs/argo-rollouts-manager v0.0.8-0.20260218104514-432c01ce417a h1:USjEzxbs2lZtx7+Hp9u5dYgu7pf/9XnDUSc9+Hmulmo= github.com/argoproj-labs/argo-rollouts-manager v0.0.8-0.20260218104514-432c01ce417a/go.mod h1:WPyZkNHZjir/OTt8mrRwcUZKe1euHrHPJsRv1Wp/F/0= -github.com/argoproj-labs/argocd-image-updater v1.1.0 h1:XR+xZf8bDFBaTpVdVpe06t/DPmrIG4BG3HukUXul6X0= -github.com/argoproj-labs/argocd-image-updater v1.1.0/go.mod h1:RbPRnEqWBPq1OP29vlZjmfL+/NfonpoagH8SInP/YHc= -github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20260211145236-4c05ef8fa3d7 h1:SF89hDvomBhku9IjRO60fzeS8ZdwHgmo7KhfTLF4tYo= -github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20260211145236-4c05ef8fa3d7/go.mod h1:G9rmG9/3gV899eg8wL/4YQYTBSq5M+xEwfVBMuE8RlA= +github.com/argoproj-labs/argocd-image-updater v1.1.1 h1:7YDaR3WX2NMsDKp0wN7TRaRRHaVHQ94tSybi2P99MGk= +github.com/argoproj-labs/argocd-image-updater v1.1.1/go.mod h1:gMHiNrGNwNSt4ljf0ykcnmNvXBk/NJ+Z17AnZVe7V7I= +github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20260225073619-a52ee52d3941 h1:wkBZFBhSxIpaOfQOwQT44kgwkI/UC7IxM85GJ8w+nHI= +github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20260225073619-a52ee52d3941/go.mod h1:3/Y9YWMU+DHC+onOQVXPAxrNkoBAGZD+UQui9BgJBjY= github.com/argoproj/argo-cd/v3 v3.3.0 h1:9UlruTd5cC/MyvorTXgAIblfZTy63MF5FYvvoAaUvwU= github.com/argoproj/argo-cd/v3 v3.3.0/go.mod h1:5VAfe0s/a4VY5GmAIFK76FtW6xn7zAcLmaw25bOL/2g= github.com/argoproj/gitops-engine v0.7.1-0.20251217140045-5baed5604d2d h1:iUJYrbSvpV9n8vyl1sBt1GceM60HhHfnHxuzcm5apDg= diff --git a/test/e2e/gitopsservice_test.go b/test/e2e/gitopsservice_test.go index 4e73a8004..4596384fb 100644 --- a/test/e2e/gitopsservice_test.go +++ b/test/e2e/gitopsservice_test.go @@ -156,7 +156,7 @@ var _ = Describe("GitOpsServiceController", func() { } By("create a new Argo CD instance in test ns") - argocdNonDefaultNamespaceInstance, err := argocd.NewCR(argocdNonDefaultInstanceName, argocdNonDefaultNamespace) + argocdNonDefaultNamespaceInstance, err := argocd.NewCR(argocdNonDefaultInstanceName, argocdNonDefaultNamespace, k8sClient) Expect(err).NotTo(HaveOccurred()) err = k8sClient.Create(context.TODO(), argocdNonDefaultNamespaceInstance) @@ -344,7 +344,7 @@ var _ = Describe("GitOpsServiceController", func() { } // create an ArgoCD instance in the source namespace - argoCDInstanceObj, err := argocd.NewCR(argocdInstance, sourceNS) + argoCDInstanceObj, err := argocd.NewCR(argocdInstance, sourceNS, k8sClient) Expect(err).NotTo(HaveOccurred()) err = k8sClient.Create(context.TODO(), argoCDInstanceObj) if !kubeerrors.IsAlreadyExists(err) { @@ -523,7 +523,7 @@ var _ = Describe("GitOpsServiceController", func() { } By("create an Argo CD instance in source namespace") - argoCDInstanceObj, err := argocd.NewCR(argocdNonDefaultNamespaceInstanceName, argocdNonDefaultNamespace) + argoCDInstanceObj, err := argocd.NewCR(argocdNonDefaultNamespaceInstanceName, argocdNonDefaultNamespace, k8sClient) Expect(err).NotTo(HaveOccurred()) err = k8sClient.Create(context.TODO(), argoCDInstanceObj) Expect(err).NotTo(HaveOccurred()) diff --git a/test/openshift/e2e/ginkgo/fixture/argocd/fixture.go b/test/openshift/e2e/ginkgo/fixture/argocd/fixture.go index 1a647b52c..239be3a33 100644 --- a/test/openshift/e2e/ginkgo/fixture/argocd/fixture.go +++ b/test/openshift/e2e/ginkgo/fixture/argocd/fixture.go @@ -153,6 +153,41 @@ func HaveApplicationControllerOperationProcessors(operationProcessors int) match }) } +func HaveExternalAuthenticationCondition(expected metav1.Condition) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + for _, c := range argocd.Status.Conditions { + // FIRST match by Type + if c.Type != expected.Type { + continue + } + GinkgoWriter.Println("Matched condition type:", c.Type) + // Then check Reason + if c.Reason != expected.Reason { + GinkgoWriter.Println("HaveCondition: reason does not match", c.Reason, expected.Reason) + return false + } + + // Then check Status + if c.Status != expected.Status { + GinkgoWriter.Println("HaveCondition: status does not match", c.Status, expected.Status) + return false + } + + // Message check is optional (can be multiline) + if expected.Message != "" && c.Message != expected.Message { + GinkgoWriter.Println("HaveCondition: message does not match") + return false + } + + // ✅ Found correct condition + return true + } + + GinkgoWriter.Println("HaveCondition: condition type not found:", expected.Type) + return false + }) +} + func HaveCondition(condition metav1.Condition) matcher.GomegaMatcher { return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { diff --git a/test/openshift/e2e/ginkgo/parallel/1-050_validate_sso_test.go b/test/openshift/e2e/ginkgo/parallel/1-050_validate_sso_test.go index 8ef203925..3835b98e1 100644 --- a/test/openshift/e2e/ginkgo/parallel/1-050_validate_sso_test.go +++ b/test/openshift/e2e/ginkgo/parallel/1-050_validate_sso_test.go @@ -2,6 +2,7 @@ package parallel import ( "context" + "strings" argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" . "github.com/onsi/ginkgo/v2" @@ -10,14 +11,27 @@ import ( 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" 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" 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" ) +func getOCPVersion() string { + output, err := osFixture.ExecCommand("oc", "version") + Expect(err).ToNot(HaveOccurred()) + for _, line := range strings.Split(output, "\n") { + if strings.Contains(line, "Server Version:") { + return strings.TrimSpace(line[strings.Index(line, ":")+1:]) + } + } + return "" +} + var _ = Describe("GitOps Operator Parallel E2E Tests", func() { Context("1-050_validate_sso", func() { @@ -44,6 +58,54 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { cleanupFunc() } }) + It("ensures the conditions in status when external Authentication is enabled on clusters; above 4.20 by default in openshit is enabled", func() { + By("creating simple namespace-scoped Argo CD instance") + ocVersion := getOCPVersion() + Expect(ocVersion).ToNot(BeEmpty()) + if ocVersion < "4.20" { + Skip("skipping this test as OCP version is less than 4.20") + return + } + ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{}, + } + argoCD.Spec.SSO = &argov1beta1api.ArgoCDSSOSpec{ + Provider: argov1beta1api.SSOProviderTypeDex, + Dex: &argov1beta1api.ArgoCDDexSpec{ + OpenShiftOAuth: true, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.HaveSSOStatus("Failed")) + + By("verifying the conditions in status") + Eventually(argoCD).Should(argocdFixture.HaveExternalAuthenticationCondition(metav1.Condition{ + Reason: "UnsupportedSSOConfiguration", + Status: "True", + Type: "UnsupportedConfiguration", + })) + + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.SSO = nil + }) + Eventually(func() []metav1.Condition { + fresh := &argov1beta1api.ArgoCD{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: argoCD.Name, Namespace: argoCD.Namespace}, fresh) + Expect(err).NotTo(HaveOccurred()) + return fresh.Status.Conditions + }, "2m", "5s").ShouldNot( + ContainElement( + WithTransform(func(c metav1.Condition) string { + return c.Type + }, Equal("UnsupportedConfiguration")), + ), + ) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.HaveSSOStatus("Unknown")) + + }) It("ensures Dex/Keycloak SSO can be enabled and disabled on a namespace-scoped Argo CD instance", func() {