diff --git a/cmd/main.go b/cmd/main.go index 3e758ac00..5ca4695a9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,6 +34,7 @@ import ( argov1alpha1api "github.com/argoproj-labs/argocd-operator/api/v1alpha1" argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" argocdcommon "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd" argocdprovisioner "github.com/argoproj-labs/argocd-operator/controllers/argocd" notificationsprovisioner "github.com/argoproj-labs/argocd-operator/controllers/notificationsconfiguration" appsv1 "github.com/openshift/api/apps/v1" @@ -56,8 +57,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook" - "github.com/argoproj-labs/argocd-operator/controllers/argocd" - pipelinesv1alpha1 "github.com/redhat-developer/gitops-operator/api/v1alpha1" "github.com/redhat-developer/gitops-operator/common" "github.com/redhat-developer/gitops-operator/controllers" @@ -211,6 +210,8 @@ func main() { } setupLog.Info(fmt.Sprintf("Watching label-selector \"%s\"", labelSelectorFlag)) + argocd.Register(openshift.ReconcilerHook, openshift.BuilderHook) + if err = (&argocdprovisioner.ReconcileArgoCD{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -257,8 +258,6 @@ func main() { os.Exit(1) } - argocd.Register(openshift.ReconcilerHook) - setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") diff --git a/controllers/argocd/openshift/openshift.go b/controllers/argocd/openshift/openshift.go index 64b0f79f1..7a743238a 100644 --- a/controllers/argocd/openshift/openshift.go +++ b/controllers/argocd/openshift/openshift.go @@ -14,10 +14,16 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + v1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/handler" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) var log = logf.Log.WithName("openshift_controller_argocd") @@ -99,6 +105,26 @@ func ReconcilerHook(cr *argoapp.ArgoCD, v interface{}, hint string) error { return nil } +// BuilderHook updates the Argo CD controller builder to watch for changes to the "admin" ClusterRole +func BuilderHook(_ *argoapp.ArgoCD, v interface{}, _ string) error { + logv := log.WithValues("module", "builder-hook") + + bldr, ok := v.(*argocd.BuilderHook) + if !ok { + return nil + } + + logv.Info("updating the Argo CD controller to watch for changes to the admin ClusterRole") + + clusterResourceHandler := handler.EnqueueRequestsFromMapFunc(adminClusterRoleMapper(bldr.Client)) + bldr.Builder.Watches(&v1.ClusterRole{}, clusterResourceHandler, + builder.WithPredicates(predicate.NewPredicateFuncs(func(o client.Object) bool { + return o.GetName() == "admin" + }))) + + return nil +} + func getPolicyRuleForApplicationController() []rbacv1.PolicyRule { return []rbacv1.PolicyRule{ { @@ -385,3 +411,33 @@ func initK8sClient() (*kubernetes.Clientset, error) { return kClient, nil } + +// adminClusterRoleMapper maps changes to the "admin" ClusterRole to all Argo CD instances in the cluster +func adminClusterRoleMapper(k8sClient client.Client) handler.MapFunc { + return func(ctx context.Context, o client.Object) []reconcile.Request { + var result = []reconcile.Request{} + + // Only process the "admin" ClusterRole + if o.GetName() != "admin" { + return result + } + + // Get all Argo CD instances in all namespaces + argocds := &argoapp.ArgoCDList{} + if err := k8sClient.List(ctx, argocds, &client.ListOptions{}); err != nil { + log.Error(err, "failed to list Argo CD instances for admin ClusterRole mapping") + return result + } + + // Create reconcile requests for all Argo CD instances + for _, argocd := range argocds.Items { + namespacedName := client.ObjectKey{ + Name: argocd.Name, + Namespace: argocd.Namespace, + } + result = append(result, reconcile.Request{NamespacedName: namespacedName}) + } + + return result + } +} diff --git a/controllers/argocd/openshift/openshift_test.go b/controllers/argocd/openshift/openshift_test.go index a0b2f6c0a..33ea59012 100644 --- a/controllers/argocd/openshift/openshift_test.go +++ b/controllers/argocd/openshift/openshift_test.go @@ -1,13 +1,20 @@ package openshift import ( + "context" + "sort" "testing" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + argoapp "github.com/argoproj-labs/argocd-operator/api/v1beta1" "github.com/stretchr/testify/assert" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) func TestReconcileArgoCD_reconcileApplicableClusterRole(t *testing.T) { @@ -207,3 +214,104 @@ func TestReconcileArgoCD_reconcileSecrets(t *testing.T) { assert.NoError(t, ReconcilerHook(a, testSecret, "")) assert.Equal(t, string(testSecret.Data["namespaces"]), "someRandomNamespace") } + +func TestAdminClusterRoleMapper(t *testing.T) { + s := scheme.Scheme + s.AddKnownTypes(argoapp.GroupVersion, &argoapp.ArgoCD{}, &argoapp.ArgoCDList{}) + + t.Run("non-admin object returns empty result", func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(s).Build() + + mapFunc := adminClusterRoleMapper(fakeClient) + + nonAdminClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-admin", + }, + } + + result := mapFunc(context.TODO(), nonAdminClusterRole) + + assert.Empty(t, result) + }) + + t.Run("admin object with no Argo CD instances returns empty result", func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(s).Build() + + mapFunc := adminClusterRoleMapper(fakeClient) + + // Create admin cluster role + adminClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin", + }, + } + + result := mapFunc(context.TODO(), adminClusterRole) + + assert.Empty(t, result) + }) + + t.Run("admin object with Argo CD instances returns reconcile requests", func(t *testing.T) { + // Create test Argo CD instances + argocd1 := &argoapp.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-1", + Namespace: "namespace-1", + }, + } + + argocd2 := &argoapp.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-2", + Namespace: "namespace-2", + }, + } + + // Create fake client with Argo CD instances + fakeClient := fake.NewClientBuilder(). + WithScheme(s). + WithObjects(argocd1, argocd2). + Build() + + mapFunc := adminClusterRoleMapper(fakeClient) + + // Create admin cluster role + adminClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admin", + }, + } + + result := mapFunc(context.TODO(), adminClusterRole) + + // Should return reconcile requests for both Argo CD instances + assert.Len(t, result, 2) + + // Check that the reconcile requests contain the correct namespaced names + expectedRequests := []reconcile.Request{ + { + NamespacedName: client.ObjectKey{ + Name: "argocd-1", + Namespace: "namespace-1", + }, + }, + { + NamespacedName: client.ObjectKey{ + Name: "argocd-2", + Namespace: "namespace-2", + }, + }, + } + + // Sort both slices to ensure consistent comparison + sort.Slice(result, func(i, j int) bool { + return result[i].NamespacedName.Name < result[j].NamespacedName.Name + }) + sort.Slice(expectedRequests, func(i, j int) bool { + return expectedRequests[i].NamespacedName.Name < expectedRequests[j].NamespacedName.Name + }) + + assert.Equal(t, expectedRequests, result) + }) +}