github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/rolebinding.go (about)

     1  package argocd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"reflect"
     8  
     9  	corev1 "k8s.io/api/core/v1"
    10  	v1 "k8s.io/api/rbac/v1"
    11  	"k8s.io/apimachinery/pkg/api/errors"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/types"
    14  	"sigs.k8s.io/controller-runtime/pkg/client"
    15  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    16  
    17  	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
    18  	"github.com/argoproj-labs/argocd-operator/common"
    19  	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
    20  )
    21  
    22  // newClusterRoleBinding returns a new ClusterRoleBinding instance.
    23  func newClusterRoleBinding(cr *argoproj.ArgoCD) *v1.ClusterRoleBinding {
    24  	return &v1.ClusterRoleBinding{
    25  		ObjectMeta: metav1.ObjectMeta{
    26  			Name:        cr.Name,
    27  			Labels:      argoutil.LabelsForCluster(cr),
    28  			Annotations: argoutil.AnnotationsForCluster(cr),
    29  		},
    30  	}
    31  }
    32  
    33  // newClusterRoleBindingWithname creates a new ClusterRoleBinding with the given name for the given ArgCD.
    34  func newClusterRoleBindingWithname(name string, cr *argoproj.ArgoCD) *v1.ClusterRoleBinding {
    35  	roleBinding := newClusterRoleBinding(cr)
    36  	roleBinding.Name = GenerateUniqueResourceName(name, cr)
    37  
    38  	labels := roleBinding.ObjectMeta.Labels
    39  	labels[common.ArgoCDKeyName] = name
    40  	roleBinding.ObjectMeta.Labels = labels
    41  
    42  	return roleBinding
    43  }
    44  
    45  // newRoleBinding returns a new RoleBinding instance.
    46  func newRoleBinding(cr *argoproj.ArgoCD) *v1.RoleBinding {
    47  	return &v1.RoleBinding{
    48  		ObjectMeta: metav1.ObjectMeta{
    49  			Name:        cr.Name,
    50  			Labels:      argoutil.LabelsForCluster(cr),
    51  			Annotations: argoutil.AnnotationsForCluster(cr),
    52  			Namespace:   cr.Namespace,
    53  		},
    54  	}
    55  }
    56  
    57  // newRoleBindingForSupportNamespaces returns a new RoleBinding instance.
    58  func newRoleBindingForSupportNamespaces(cr *argoproj.ArgoCD, namespace string) *v1.RoleBinding {
    59  	return &v1.RoleBinding{
    60  		ObjectMeta: metav1.ObjectMeta{
    61  			Name:        getRoleBindingNameForSourceNamespaces(cr.Name, namespace),
    62  			Labels:      argoutil.LabelsForCluster(cr),
    63  			Annotations: argoutil.AnnotationsForCluster(cr),
    64  			Namespace:   namespace,
    65  		},
    66  	}
    67  }
    68  
    69  func getRoleBindingNameForSourceNamespaces(argocdName, targetNamespace string) string {
    70  	return fmt.Sprintf("%s_%s", argocdName, targetNamespace)
    71  }
    72  
    73  // newRoleBindingWithname creates a new RoleBinding with the given name for the given ArgCD.
    74  func newRoleBindingWithname(name string, cr *argoproj.ArgoCD) *v1.RoleBinding {
    75  	roleBinding := newRoleBinding(cr)
    76  	roleBinding.ObjectMeta.Name = fmt.Sprintf("%s-%s", cr.Name, name)
    77  
    78  	labels := roleBinding.ObjectMeta.Labels
    79  	labels[common.ArgoCDKeyName] = name
    80  	roleBinding.ObjectMeta.Labels = labels
    81  
    82  	return roleBinding
    83  }
    84  
    85  // reconcileRoleBindings will ensure that all ArgoCD RoleBindings are configured.
    86  func (r *ReconcileArgoCD) reconcileRoleBindings(cr *argoproj.ArgoCD) error {
    87  	params := getPolicyRuleList(r.Client)
    88  
    89  	for _, param := range params {
    90  		if err := r.reconcileRoleBinding(param.name, param.policyRule, cr); err != nil {
    91  			return fmt.Errorf("error reconciling roleBinding for %q: %w", param.name, err)
    92  		}
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  // reconcileRoleBinding, creates RoleBindings for every role and associates it with the right ServiceAccount.
    99  // This would create RoleBindings for all the namespaces managed by the ArgoCD instance.
   100  func (r *ReconcileArgoCD) reconcileRoleBinding(name string, rules []v1.PolicyRule, cr *argoproj.ArgoCD) error {
   101  	var sa *corev1.ServiceAccount
   102  	var error error
   103  
   104  	if sa, error = r.reconcileServiceAccount(name, cr); error != nil {
   105  		return error
   106  	}
   107  
   108  	if _, error = r.reconcileRole(name, rules, cr); error != nil {
   109  		return error
   110  	}
   111  
   112  	for _, namespace := range r.ManagedNamespaces.Items {
   113  		// If encountering a terminating namespace remove managed-by label from it and skip reconciliation - This should trigger
   114  		// clean-up of roles/rolebindings and removal of namespace from cluster secret
   115  		if namespace.DeletionTimestamp != nil {
   116  			if _, ok := namespace.Labels[common.ArgoCDManagedByLabel]; ok {
   117  				delete(namespace.Labels, common.ArgoCDManagedByLabel)
   118  				_ = r.Client.Update(context.TODO(), &namespace)
   119  			}
   120  			continue
   121  		}
   122  
   123  		list := &argoproj.ArgoCDList{}
   124  		listOption := &client.ListOptions{Namespace: namespace.Name}
   125  		err := r.Client.List(context.TODO(), list, listOption)
   126  		if err != nil {
   127  			return err
   128  		}
   129  		// only skip creation of dex and redisHa rolebindings for namespaces that no argocd instance is deployed in
   130  		if len(list.Items) < 1 {
   131  			// namespace doesn't contain argocd instance, so skipe all the ArgoCD internal roles
   132  			if cr.ObjectMeta.Namespace != namespace.Name && (name != common.ArgoCDApplicationControllerComponent && name != common.ArgoCDServerComponent) {
   133  				continue
   134  			}
   135  		}
   136  		// get expected name
   137  		roleBinding := newRoleBindingWithname(name, cr)
   138  		roleBinding.Namespace = namespace.Name
   139  
   140  		// fetch existing rolebinding by name
   141  		existingRoleBinding := &v1.RoleBinding{}
   142  		err = r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, existingRoleBinding)
   143  		roleBindingExists := true
   144  		if err != nil {
   145  			if !errors.IsNotFound(err) {
   146  				return fmt.Errorf("failed to get the rolebinding associated with %s : %s", name, err)
   147  			}
   148  
   149  			if name == common.ArgoCDDexServerComponent && !UseDex(cr) {
   150  				continue // Dex installation is not requested, do nothing
   151  			}
   152  
   153  			roleBindingExists = false
   154  		}
   155  
   156  		roleBinding.Subjects = []v1.Subject{
   157  			{
   158  				Kind:      v1.ServiceAccountKind,
   159  				Name:      sa.Name,
   160  				Namespace: sa.Namespace,
   161  			},
   162  		}
   163  
   164  		customRoleName := getCustomRoleName(name)
   165  		if customRoleName != "" {
   166  			roleBinding.RoleRef = v1.RoleRef{
   167  				APIGroup: v1.GroupName,
   168  				Kind:     "ClusterRole",
   169  				Name:     customRoleName,
   170  			}
   171  		} else {
   172  			roleBinding.RoleRef = v1.RoleRef{
   173  				APIGroup: v1.GroupName,
   174  				Kind:     "Role",
   175  				Name:     generateResourceName(name, cr),
   176  			}
   177  		}
   178  
   179  		if roleBindingExists {
   180  			if name == common.ArgoCDDexServerComponent && !UseDex(cr) {
   181  				// Delete any existing RoleBinding created for Dex since dex uninstallation is requested
   182  				log.Info("deleting the existing Dex roleBinding because dex uninstallation is requested")
   183  				if err = r.Client.Delete(context.TODO(), existingRoleBinding); err != nil {
   184  					return err
   185  				}
   186  				continue
   187  			}
   188  
   189  			// if the RoleRef changes, delete the existing role binding and create a new one
   190  			if !reflect.DeepEqual(roleBinding.RoleRef, existingRoleBinding.RoleRef) {
   191  				if err = r.Client.Delete(context.TODO(), existingRoleBinding); err != nil {
   192  					return err
   193  				}
   194  			} else {
   195  				// if the Subjects differ, update the role bindings
   196  				if !reflect.DeepEqual(roleBinding.Subjects, existingRoleBinding.Subjects) {
   197  					existingRoleBinding.Subjects = roleBinding.Subjects
   198  					if err = r.Client.Update(context.TODO(), existingRoleBinding); err != nil {
   199  						return err
   200  					}
   201  				}
   202  				continue
   203  			}
   204  		}
   205  
   206  		// Only set ownerReferences for role bindings in same namespaces as Argo CD CR
   207  		if cr.Namespace == roleBinding.Namespace {
   208  			if err = controllerutil.SetControllerReference(cr, roleBinding, r.Scheme); err != nil {
   209  				return fmt.Errorf("failed to set ArgoCD CR \"%s\" as owner for roleBinding \"%s\": %s", cr.Name, roleBinding.Name, err)
   210  			}
   211  		}
   212  
   213  		log.Info(fmt.Sprintf("creating rolebinding %s for Argo CD instance %s in namespace %s", roleBinding.Name, cr.Name, cr.Namespace))
   214  		if err = r.Client.Create(context.TODO(), roleBinding); err != nil {
   215  			return err
   216  		}
   217  	}
   218  
   219  	// reconcile rolebindings only for ArgoCDServerComponent
   220  	if name == common.ArgoCDServerComponent {
   221  
   222  		// reconcile rolebindings for all source namespaces for argocd-server
   223  		sourceNamespaces, err := r.getSourceNamespaces(cr)
   224  		if err != nil {
   225  			return err
   226  		}
   227  		for _, sourceNamespace := range sourceNamespaces {
   228  			namespace := &corev1.Namespace{}
   229  			if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: sourceNamespace}, namespace); err != nil {
   230  				return err
   231  			}
   232  
   233  			// do not reconcile rolebindings for namespaces already containing managed-by label
   234  			// as it already contains rolebindings with permissions to manipulate application resources
   235  			// reconciled during reconcilation of ManagedNamespaces
   236  			if value, ok := namespace.Labels[common.ArgoCDManagedByLabel]; ok {
   237  				log.Info(fmt.Sprintf("Skipping reconciling resources for namespace %s as it is already managed-by namespace %s.", namespace.Name, value))
   238  				continue
   239  			}
   240  
   241  			list := &argoproj.ArgoCDList{}
   242  			listOption := &client.ListOptions{Namespace: namespace.Name}
   243  			err := r.Client.List(context.TODO(), list, listOption)
   244  			if err != nil {
   245  				log.Info(err.Error())
   246  				return err
   247  			}
   248  
   249  			// get expected name
   250  			roleBinding := newRoleBindingWithNameForApplicationSourceNamespaces(namespace.Name, cr)
   251  			roleBinding.Namespace = namespace.Name
   252  
   253  			roleBinding.RoleRef = v1.RoleRef{
   254  				APIGroup: v1.GroupName,
   255  				Kind:     "Role",
   256  				Name:     getRoleNameForApplicationSourceNamespaces(namespace.Name, cr),
   257  			}
   258  
   259  			// fetch existing rolebinding by name
   260  			existingRoleBinding := &v1.RoleBinding{}
   261  			err = r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, existingRoleBinding)
   262  			roleBindingExists := true
   263  			if err != nil {
   264  				if !errors.IsNotFound(err) {
   265  					return fmt.Errorf("failed to get the rolebinding associated with %s : %s", name, err)
   266  				}
   267  				log.Info(fmt.Sprintf("Existing rolebinding %s", err.Error()))
   268  				roleBindingExists = false
   269  			}
   270  
   271  			roleBinding.Subjects = []v1.Subject{
   272  				{
   273  					Kind:      v1.ServiceAccountKind,
   274  					Name:      getServiceAccountName(cr.Name, common.ArgoCDServerComponent),
   275  					Namespace: cr.Namespace,
   276  				},
   277  				{
   278  					Kind:      v1.ServiceAccountKind,
   279  					Name:      getServiceAccountName(cr.Name, common.ArgoCDApplicationControllerComponent),
   280  					Namespace: cr.Namespace,
   281  				},
   282  			}
   283  
   284  			if roleBindingExists {
   285  				// reconcile role bindings for namespaces already containing managed-by-cluster-argocd label only
   286  				if n, ok := namespace.Labels[common.ArgoCDManagedByClusterArgoCDLabel]; !ok || n != cr.Namespace {
   287  					continue
   288  				}
   289  				// if the RoleRef changes, delete the existing role binding and create a new one
   290  				if !reflect.DeepEqual(roleBinding.RoleRef, existingRoleBinding.RoleRef) {
   291  					if err = r.Client.Delete(context.TODO(), existingRoleBinding); err != nil {
   292  						return err
   293  					}
   294  				} else {
   295  					// if the Subjects differ, update the role bindings
   296  					if !reflect.DeepEqual(roleBinding.Subjects, existingRoleBinding.Subjects) {
   297  						existingRoleBinding.Subjects = roleBinding.Subjects
   298  						if err = r.Client.Update(context.TODO(), existingRoleBinding); err != nil {
   299  							return err
   300  						}
   301  					}
   302  					continue
   303  				}
   304  			}
   305  
   306  			log.Info(fmt.Sprintf("creating rolebinding %s for Argo CD instance %s in namespace %s", roleBinding.Name, cr.Name, namespace))
   307  			if err = r.Client.Create(context.TODO(), roleBinding); err != nil {
   308  				return err
   309  			}
   310  		}
   311  	}
   312  	return nil
   313  }
   314  
   315  func getCustomRoleName(name string) string {
   316  	if name == common.ArgoCDApplicationControllerComponent {
   317  		return os.Getenv(common.ArgoCDControllerClusterRoleEnvName)
   318  	}
   319  	if name == common.ArgoCDServerComponent {
   320  		return os.Getenv(common.ArgoCDServerClusterRoleEnvName)
   321  	}
   322  	return ""
   323  }
   324  
   325  // Returns the name of the role for the source namespaces for ArgoCDServer in the format of "sourceNamespace_targetNamespace_argocd-server"
   326  func getRoleNameForApplicationSourceNamespaces(targetNamespace string, cr *argoproj.ArgoCD) string {
   327  	return fmt.Sprintf("%s_%s", cr.Name, targetNamespace)
   328  }
   329  
   330  // newRoleBindingWithNameForApplicationSourceNamespaces creates a new RoleBinding with the given name for the source namespaces of ArgoCD Server.
   331  func newRoleBindingWithNameForApplicationSourceNamespaces(namespace string, cr *argoproj.ArgoCD) *v1.RoleBinding {
   332  	roleBinding := newRoleBindingForSupportNamespaces(cr, namespace)
   333  
   334  	labels := roleBinding.ObjectMeta.Labels
   335  	labels[common.ArgoCDKeyName] = roleBinding.ObjectMeta.Name
   336  	roleBinding.ObjectMeta.Labels = labels
   337  
   338  	return roleBinding
   339  }
   340  
   341  func (r *ReconcileArgoCD) reconcileClusterRoleBinding(name string, role *v1.ClusterRole, cr *argoproj.ArgoCD) error {
   342  
   343  	// get expected name
   344  	roleBinding := newClusterRoleBindingWithname(name, cr)
   345  	// fetch existing rolebinding by name
   346  	err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name}, roleBinding)
   347  	roleBindingExists := true
   348  	if err != nil {
   349  		if !errors.IsNotFound(err) {
   350  			return err
   351  		}
   352  		roleBindingExists = false
   353  		roleBinding = newClusterRoleBindingWithname(name, cr)
   354  	}
   355  
   356  	if roleBindingExists && role == nil {
   357  		return r.Client.Delete(context.TODO(), roleBinding)
   358  	}
   359  
   360  	if !roleBindingExists && role == nil {
   361  		// DO Nothing
   362  		return nil
   363  	}
   364  
   365  	roleBinding.Subjects = []v1.Subject{
   366  		{
   367  			Kind:      v1.ServiceAccountKind,
   368  			Name:      generateResourceName(name, cr),
   369  			Namespace: cr.Namespace,
   370  		},
   371  	}
   372  	roleBinding.RoleRef = v1.RoleRef{
   373  		APIGroup: v1.GroupName,
   374  		Kind:     "ClusterRole",
   375  		Name:     GenerateUniqueResourceName(name, cr),
   376  	}
   377  
   378  	if cr.Namespace == roleBinding.Namespace {
   379  		if err = controllerutil.SetControllerReference(cr, roleBinding, r.Scheme); err != nil {
   380  			return fmt.Errorf("failed to set ArgoCD CR \"%s\" as owner for roleBinding \"%s\": %s", cr.Name, roleBinding.Name, err)
   381  		}
   382  	}
   383  
   384  	if roleBindingExists {
   385  		return r.Client.Update(context.TODO(), roleBinding)
   386  	}
   387  	return r.Client.Create(context.TODO(), roleBinding)
   388  }
   389  
   390  func deleteClusterRoleBindings(c client.Client, clusterBindingList *v1.ClusterRoleBindingList) error {
   391  	for _, clusterBinding := range clusterBindingList.Items {
   392  		if err := c.Delete(context.TODO(), &clusterBinding); err != nil {
   393  			return fmt.Errorf("failed to delete ClusterRoleBinding %q during cleanup: %w", clusterBinding.Name, err)
   394  		}
   395  	}
   396  	return nil
   397  }