github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/role.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  // newRole returns a new Role instance.
    23  func newRole(name string, rules []v1.PolicyRule, cr *argoproj.ArgoCD) *v1.Role {
    24  	return &v1.Role{
    25  		ObjectMeta: metav1.ObjectMeta{
    26  			Name:      generateResourceName(name, cr),
    27  			Namespace: cr.Namespace,
    28  			Labels:    argoutil.LabelsForCluster(cr),
    29  		},
    30  		Rules: rules,
    31  	}
    32  }
    33  
    34  func newRoleForApplicationSourceNamespaces(namespace string, rules []v1.PolicyRule, cr *argoproj.ArgoCD) *v1.Role {
    35  	return &v1.Role{
    36  		ObjectMeta: metav1.ObjectMeta{
    37  			Name:      getRoleNameForApplicationSourceNamespaces(namespace, cr),
    38  			Namespace: namespace,
    39  			Labels:    argoutil.LabelsForCluster(cr),
    40  		},
    41  		Rules: rules,
    42  	}
    43  }
    44  
    45  func generateResourceName(argoComponentName string, cr *argoproj.ArgoCD) string {
    46  	return cr.Name + "-" + argoComponentName
    47  }
    48  
    49  // GenerateUniqueResourceName generates unique names for cluster scoped resources
    50  func GenerateUniqueResourceName(argoComponentName string, cr *argoproj.ArgoCD) string {
    51  	return cr.Name + "-" + cr.Namespace + "-" + argoComponentName
    52  }
    53  
    54  func newClusterRole(name string, rules []v1.PolicyRule, cr *argoproj.ArgoCD) *v1.ClusterRole {
    55  	return &v1.ClusterRole{
    56  		ObjectMeta: metav1.ObjectMeta{
    57  			Name:        GenerateUniqueResourceName(name, cr),
    58  			Labels:      argoutil.LabelsForCluster(cr),
    59  			Annotations: argoutil.AnnotationsForCluster(cr),
    60  		},
    61  		Rules: rules,
    62  	}
    63  }
    64  
    65  // reconcileRoles will ensure that all ArgoCD Service Accounts are configured.
    66  func (r *ReconcileArgoCD) reconcileRoles(cr *argoproj.ArgoCD) error {
    67  	params := getPolicyRuleList(r.Client)
    68  
    69  	for _, param := range params {
    70  		if _, err := r.reconcileRole(param.name, param.policyRule, cr); err != nil {
    71  			return err
    72  		}
    73  	}
    74  
    75  	clusterParams := getPolicyRuleClusterRoleList()
    76  
    77  	for _, clusterParam := range clusterParams {
    78  		if _, err := r.reconcileClusterRole(clusterParam.name, clusterParam.policyRule, cr); err != nil {
    79  			return err
    80  		}
    81  	}
    82  
    83  	log.Info("reconciling roles for source namespaces")
    84  	policyRuleForApplicationSourceNamespaces := policyRuleForServerApplicationSourceNamespaces()
    85  	// reconcile roles is source namespaces for ArgoCD Server
    86  	if err := r.reconcileRoleForApplicationSourceNamespaces(common.ArgoCDServerComponent, policyRuleForApplicationSourceNamespaces, cr); err != nil {
    87  		return err
    88  	}
    89  
    90  	log.Info("performing cleanup for source namespaces")
    91  	// remove resources for namespaces not part of SourceNamespaces
    92  	if err := r.removeUnmanagedSourceNamespaceResources(cr); err != nil {
    93  		return err
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  // reconcileRole, reconciles the policy rules for different ArgoCD components, for each namespace
   100  // Managed by a single instance of ArgoCD.
   101  func (r *ReconcileArgoCD) reconcileRole(name string, policyRules []v1.PolicyRule, cr *argoproj.ArgoCD) ([]*v1.Role, error) {
   102  	var roles []*v1.Role
   103  
   104  	// create policy rules for each namespace
   105  	for _, namespace := range r.ManagedNamespaces.Items {
   106  		// If encountering a terminating namespace remove managed-by label from it and skip reconciliation - This should trigger
   107  		// clean-up of roles/rolebindings and removal of namespace from cluster secret
   108  		if namespace.DeletionTimestamp != nil {
   109  			if _, ok := namespace.Labels[common.ArgoCDManagedByLabel]; ok {
   110  				delete(namespace.Labels, common.ArgoCDManagedByLabel)
   111  				_ = r.Client.Update(context.TODO(), &namespace)
   112  			}
   113  			continue
   114  		}
   115  
   116  		list := &argoproj.ArgoCDList{}
   117  		listOption := &client.ListOptions{Namespace: namespace.Name}
   118  		err := r.Client.List(context.TODO(), list, listOption)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  		// only skip creation of dex and redisHa roles for namespaces that no argocd instance is deployed in
   123  		if len(list.Items) < 1 {
   124  			// namespace doesn't contain argocd instance, so skipe all the ArgoCD internal roles
   125  			if cr.ObjectMeta.Namespace != namespace.Name && (name != common.ArgoCDApplicationControllerComponent && name != common.ArgoCDServerComponent) {
   126  				continue
   127  			}
   128  		}
   129  		customRole := getCustomRoleName(name)
   130  		role := newRole(name, policyRules, cr)
   131  		if err := applyReconcilerHook(cr, role, ""); err != nil {
   132  			return nil, err
   133  		}
   134  		role.Namespace = namespace.Name
   135  		existingRole := v1.Role{}
   136  		err = r.Client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, &existingRole)
   137  		if err != nil {
   138  			if !errors.IsNotFound(err) {
   139  				return nil, fmt.Errorf("failed to reconcile the role for the service account associated with %s : %s", name, err)
   140  			}
   141  			if customRole != "" {
   142  				continue // skip creating default role if custom cluster role is provided
   143  			}
   144  			roles = append(roles, role)
   145  
   146  			if name == common.ArgoCDDexServerComponent && !UseDex(cr) {
   147  
   148  				continue // Dex installation not requested, do nothing
   149  			}
   150  
   151  			// Only set ownerReferences for roles in same namespace as ArgoCD CR
   152  			if cr.Namespace == role.Namespace {
   153  				if err = controllerutil.SetControllerReference(cr, role, r.Scheme); err != nil {
   154  					return nil, fmt.Errorf("failed to set ArgoCD CR \"%s\" as owner for role \"%s\": %s", cr.Name, role.Name, err)
   155  				}
   156  			}
   157  
   158  			log.Info(fmt.Sprintf("creating role %s for Argo CD instance %s in namespace %s", role.Name, cr.Name, cr.Namespace))
   159  			if err := r.Client.Create(context.TODO(), role); err != nil {
   160  				return nil, err
   161  			}
   162  			continue
   163  		}
   164  
   165  		// Delete the existing default role if custom role is specified
   166  		// or if there is an existing Role created for Dex but dex is disabled or not configured
   167  		if customRole != "" ||
   168  			(name == common.ArgoCDDexServerComponent && !UseDex(cr)) {
   169  
   170  			log.Info("deleting the existing Dex role because dex is not configured")
   171  			if err := r.Client.Delete(context.TODO(), &existingRole); err != nil {
   172  				return nil, err
   173  			}
   174  			continue
   175  		}
   176  
   177  		// if the Rules differ, update the Role
   178  		if !reflect.DeepEqual(existingRole.Rules, role.Rules) {
   179  			existingRole.Rules = role.Rules
   180  			if err := r.Client.Update(context.TODO(), &existingRole); err != nil {
   181  				return nil, err
   182  			}
   183  		}
   184  		roles = append(roles, &existingRole)
   185  	}
   186  	return roles, nil
   187  }
   188  
   189  func (r *ReconcileArgoCD) reconcileRoleForApplicationSourceNamespaces(name string, policyRules []v1.PolicyRule, cr *argoproj.ArgoCD) error {
   190  
   191  	// create policy rules for each source namespace for ArgoCD Server
   192  	sourceNamespaces, err := r.getSourceNamespaces(cr)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	for _, sourceNamespace := range sourceNamespaces {
   198  		namespace := &corev1.Namespace{}
   199  		if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: sourceNamespace}, namespace); err != nil {
   200  			return err
   201  		}
   202  		// do not reconcile roles for namespaces already containing managed-by label
   203  		// as it already contains roles with permissions to manipulate application resources
   204  		// reconciled during reconcilation of ManagedNamespaces
   205  		if value, ok := namespace.Labels[common.ArgoCDManagedByLabel]; ok && value != "" {
   206  			log.Info(fmt.Sprintf("Skipping reconciling resources for namespace %s as it is already managed-by namespace %s.", namespace.Name, value))
   207  			// if managed-by-cluster-argocd label is also present, remove the namespace from the ManagedSourceNamespaces.
   208  			if val, ok1 := namespace.Labels[common.ArgoCDManagedByClusterArgoCDLabel]; ok1 && val == cr.Namespace {
   209  				delete(r.ManagedSourceNamespaces, namespace.Name)
   210  				if err := r.cleanupUnmanagedSourceNamespaceResources(cr, namespace.Name); err != nil {
   211  					log.Error(err, fmt.Sprintf("error cleaning up resources for namespace %s", namespace.Name))
   212  				}
   213  			}
   214  			continue
   215  		}
   216  
   217  		// reconcile roles only if another ArgoCD instance is not already set as value for managed-by-cluster-argocd label
   218  		if value, ok := namespace.Labels[common.ArgoCDManagedByClusterArgoCDLabel]; ok && value != cr.Namespace {
   219  			log.Info(fmt.Sprintf("Namespace already has label set to argocd instance %s. Thus, skipping namespace %s", value, namespace.Name))
   220  			continue
   221  		}
   222  
   223  		log.Info(fmt.Sprintf("Reconciling role for %s", namespace.Name))
   224  
   225  		role := newRoleForApplicationSourceNamespaces(namespace.Name, policyRules, cr)
   226  		if err := applyReconcilerHook(cr, role, ""); err != nil {
   227  			return err
   228  		}
   229  		role.Namespace = namespace.Name
   230  		// patch rules if appset in source namespace is allowed
   231  		if contains(r.getApplicationSetSourceNamespaces(cr), sourceNamespace) {
   232  			role.Rules = append(role.Rules, policyRuleForServerApplicationSetSourceNamespaces()...)
   233  		}
   234  
   235  		created := false
   236  		existingRole := v1.Role{}
   237  		err := r.Client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: namespace.Name}, &existingRole)
   238  		if err != nil {
   239  			if !errors.IsNotFound(err) {
   240  				return fmt.Errorf("failed to reconcile the role for the service account associated with %s : %s", name, err)
   241  			}
   242  
   243  			log.Info(fmt.Sprintf("creating role %s for Argo CD instance %s in namespace %s", role.Name, cr.Name, namespace))
   244  			if err := r.Client.Create(context.TODO(), role); err != nil {
   245  				return err
   246  			}
   247  			created = true
   248  		}
   249  
   250  		// Get the latest value of namespace before updating it
   251  		if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: namespace.Name}, namespace); err != nil {
   252  			return err
   253  		}
   254  		// Update namespace with managed-by-cluster-argocd label
   255  		if namespace.Labels == nil {
   256  			namespace.Labels = make(map[string]string)
   257  		}
   258  		namespace.Labels[common.ArgoCDManagedByClusterArgoCDLabel] = cr.Namespace
   259  		if err := r.Client.Update(context.TODO(), namespace); err != nil {
   260  			log.Error(err, fmt.Sprintf("failed to add label from namespace [%s]", namespace.Name))
   261  		}
   262  
   263  		// if the Rules differ, update the Role
   264  		if !created && !reflect.DeepEqual(existingRole.Rules, role.Rules) {
   265  			existingRole.Rules = role.Rules
   266  			if err := r.Client.Update(context.TODO(), &existingRole); err != nil {
   267  				return err
   268  			}
   269  		}
   270  
   271  		if _, ok := r.ManagedSourceNamespaces[sourceNamespace]; !ok {
   272  			if r.ManagedSourceNamespaces == nil {
   273  				r.ManagedSourceNamespaces = make(map[string]string)
   274  			}
   275  			r.ManagedSourceNamespaces[sourceNamespace] = ""
   276  		}
   277  
   278  	}
   279  	return nil
   280  }
   281  
   282  func (r *ReconcileArgoCD) reconcileClusterRole(name string, policyRules []v1.PolicyRule, cr *argoproj.ArgoCD) (*v1.ClusterRole, error) {
   283  	allowed := false
   284  	if allowedNamespace(cr.Namespace, os.Getenv("ARGOCD_CLUSTER_CONFIG_NAMESPACES")) {
   285  		allowed = true
   286  	}
   287  	clusterRole := newClusterRole(name, policyRules, cr)
   288  	if err := applyReconcilerHook(cr, clusterRole, ""); err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	existingClusterRole := &v1.ClusterRole{}
   293  	err := r.Client.Get(context.TODO(), types.NamespacedName{Name: clusterRole.Name}, existingClusterRole)
   294  	if err != nil {
   295  		if !errors.IsNotFound(err) {
   296  			return nil, fmt.Errorf("failed to reconcile the cluster role for the service account associated with %s : %s", name, err)
   297  		}
   298  		if !allowed {
   299  			// Do Nothing
   300  			return nil, nil
   301  		}
   302  		return clusterRole, r.Client.Create(context.TODO(), clusterRole)
   303  	}
   304  
   305  	if !allowed {
   306  		return nil, r.Client.Delete(context.TODO(), existingClusterRole)
   307  	}
   308  
   309  	// if the Rules differ, update the Role
   310  	if !reflect.DeepEqual(existingClusterRole.Rules, clusterRole.Rules) {
   311  		existingClusterRole.Rules = clusterRole.Rules
   312  		if err := r.Client.Update(context.TODO(), existingClusterRole); err != nil {
   313  			return nil, err
   314  		}
   315  	}
   316  	return existingClusterRole, nil
   317  }
   318  
   319  func deleteClusterRoles(c client.Client, clusterRoleList *v1.ClusterRoleList) error {
   320  	for _, clusterRole := range clusterRoleList.Items {
   321  		if err := c.Delete(context.TODO(), &clusterRole); err != nil {
   322  			return fmt.Errorf("failed to delete ClusterRole %q during cleanup: %w", clusterRole.Name, err)
   323  		}
   324  	}
   325  	return nil
   326  }