github.com/argoproj/argo-cd/v2@v2.10.9/server/applicationset/applicationset.go (about)

     1  package applicationset
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/argoproj/pkg/sync"
    13  	log "github.com/sirupsen/logrus"
    14  	"google.golang.org/grpc/codes"
    15  	"google.golang.org/grpc/status"
    16  	v1 "k8s.io/api/core/v1"
    17  	apierr "k8s.io/apimachinery/pkg/api/errors"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/labels"
    20  	"k8s.io/client-go/kubernetes"
    21  	"k8s.io/client-go/tools/cache"
    22  
    23  	appsetutils "github.com/argoproj/argo-cd/v2/applicationset/utils"
    24  	"github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset"
    25  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    26  	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
    27  	applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
    28  	"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
    29  	"github.com/argoproj/argo-cd/v2/util/argo"
    30  	"github.com/argoproj/argo-cd/v2/util/collections"
    31  	"github.com/argoproj/argo-cd/v2/util/db"
    32  	"github.com/argoproj/argo-cd/v2/util/rbac"
    33  	"github.com/argoproj/argo-cd/v2/util/security"
    34  	"github.com/argoproj/argo-cd/v2/util/session"
    35  	"github.com/argoproj/argo-cd/v2/util/settings"
    36  )
    37  
    38  type Server struct {
    39  	ns                string
    40  	db                db.ArgoDB
    41  	enf               *rbac.Enforcer
    42  	appclientset      appclientset.Interface
    43  	appsetInformer    cache.SharedIndexInformer
    44  	appsetLister      applisters.ApplicationSetLister
    45  	projLister        applisters.AppProjectNamespaceLister
    46  	auditLogger       *argo.AuditLogger
    47  	settings          *settings.SettingsManager
    48  	projectLock       sync.KeyLock
    49  	enabledNamespaces []string
    50  }
    51  
    52  // NewServer returns a new instance of the ApplicationSet service
    53  func NewServer(
    54  	db db.ArgoDB,
    55  	kubeclientset kubernetes.Interface,
    56  	enf *rbac.Enforcer,
    57  	appclientset appclientset.Interface,
    58  	appsetInformer cache.SharedIndexInformer,
    59  	appsetLister applisters.ApplicationSetLister,
    60  	projLister applisters.AppProjectNamespaceLister,
    61  	settings *settings.SettingsManager,
    62  	namespace string,
    63  	projectLock sync.KeyLock,
    64  	enabledNamespaces []string,
    65  ) applicationset.ApplicationSetServiceServer {
    66  	s := &Server{
    67  		ns:                namespace,
    68  		db:                db,
    69  		enf:               enf,
    70  		appclientset:      appclientset,
    71  		appsetInformer:    appsetInformer,
    72  		appsetLister:      appsetLister,
    73  		projLister:        projLister,
    74  		settings:          settings,
    75  		projectLock:       projectLock,
    76  		auditLogger:       argo.NewAuditLogger(namespace, kubeclientset, "argocd-server"),
    77  		enabledNamespaces: enabledNamespaces,
    78  	}
    79  	return s
    80  }
    81  
    82  func (s *Server) Get(ctx context.Context, q *applicationset.ApplicationSetGetQuery) (*v1alpha1.ApplicationSet, error) {
    83  
    84  	namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
    85  
    86  	if !s.isNamespaceEnabled(namespace) {
    87  		return nil, security.NamespaceNotPermittedError(namespace)
    88  	}
    89  
    90  	a, err := s.appsetLister.ApplicationSets(namespace).Get(q.Name)
    91  
    92  	if err != nil {
    93  		return nil, fmt.Errorf("error getting ApplicationSet: %w", err)
    94  	}
    95  	if err = s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionGet, a.RBACName(s.ns)); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return a, nil
   100  }
   101  
   102  // List returns list of ApplicationSets
   103  func (s *Server) List(ctx context.Context, q *applicationset.ApplicationSetListQuery) (*v1alpha1.ApplicationSetList, error) {
   104  	selector, err := labels.Parse(q.GetSelector())
   105  	if err != nil {
   106  		return nil, fmt.Errorf("error parsing the selector: %w", err)
   107  	}
   108  
   109  	var appsets []*v1alpha1.ApplicationSet
   110  	if q.AppsetNamespace == "" {
   111  		appsets, err = s.appsetLister.List(selector)
   112  	} else {
   113  		appsets, err = s.appsetLister.ApplicationSets(q.AppsetNamespace).List(selector)
   114  	}
   115  
   116  	if err != nil {
   117  		return nil, fmt.Errorf("error listing ApplicationSets with selectors: %w", err)
   118  	}
   119  
   120  	newItems := make([]v1alpha1.ApplicationSet, 0)
   121  	for _, a := range appsets {
   122  
   123  		// Skip any application that is neither in the conrol plane's namespace
   124  		// nor in the list of enabled namespaces.
   125  		if !security.IsNamespaceEnabled(a.Namespace, s.ns, s.enabledNamespaces) {
   126  			continue
   127  		}
   128  
   129  		if s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionGet, a.RBACName(s.ns)) {
   130  			newItems = append(newItems, *a)
   131  		}
   132  	}
   133  
   134  	newItems = argo.FilterAppSetsByProjects(newItems, q.Projects)
   135  
   136  	// Sort found applicationsets by name
   137  	sort.Slice(newItems, func(i, j int) bool {
   138  		return newItems[i].Name < newItems[j].Name
   139  	})
   140  
   141  	appsetList := &v1alpha1.ApplicationSetList{
   142  		ListMeta: metav1.ListMeta{
   143  			ResourceVersion: s.appsetInformer.LastSyncResourceVersion(),
   144  		},
   145  		Items: newItems,
   146  	}
   147  	return appsetList, nil
   148  
   149  }
   150  
   151  func (s *Server) Create(ctx context.Context, q *applicationset.ApplicationSetCreateRequest) (*v1alpha1.ApplicationSet, error) {
   152  	appset := q.GetApplicationset()
   153  
   154  	if appset == nil {
   155  		return nil, fmt.Errorf("error creating ApplicationSets: ApplicationSets is nil in request")
   156  	}
   157  
   158  	projectName, err := s.validateAppSet(ctx, appset)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("error validating ApplicationSets: %w", err)
   161  	}
   162  
   163  	namespace := s.appsetNamespaceOrDefault(appset.Namespace)
   164  
   165  	if !s.isNamespaceEnabled(namespace) {
   166  		return nil, security.NamespaceNotPermittedError(namespace)
   167  	}
   168  
   169  	if err := s.checkCreatePermissions(ctx, appset, projectName); err != nil {
   170  		return nil, fmt.Errorf("error checking create permissions for ApplicationSets %s : %s", appset.Name, err)
   171  	}
   172  
   173  	s.projectLock.RLock(projectName)
   174  	defer s.projectLock.RUnlock(projectName)
   175  
   176  	created, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Create(ctx, appset, metav1.CreateOptions{})
   177  	if err == nil {
   178  		s.logAppSetEvent(created, ctx, argo.EventReasonResourceCreated, "created ApplicationSet")
   179  		s.waitSync(created)
   180  		return created, nil
   181  	}
   182  
   183  	if !apierr.IsAlreadyExists(err) {
   184  		return nil, fmt.Errorf("error creating ApplicationSet: %w", err)
   185  	}
   186  	// act idempotent if existing spec matches new spec
   187  	existing, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(s.ns).Get(ctx, appset.Name, metav1.GetOptions{
   188  		ResourceVersion: "",
   189  	})
   190  	if err != nil {
   191  		return nil, status.Errorf(codes.Internal, "unable to check existing ApplicationSet details: %v", err)
   192  	}
   193  
   194  	equalSpecs := reflect.DeepEqual(existing.Spec, appset.Spec) &&
   195  		reflect.DeepEqual(existing.Labels, appset.Labels) &&
   196  		reflect.DeepEqual(existing.Annotations, appset.Annotations) &&
   197  		reflect.DeepEqual(existing.Finalizers, appset.Finalizers)
   198  
   199  	if equalSpecs {
   200  		return existing, nil
   201  	}
   202  
   203  	if !q.Upsert {
   204  		return nil, status.Errorf(codes.InvalidArgument, "existing ApplicationSet spec is different, use upsert flag to force update")
   205  	}
   206  	if err = s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionUpdate, appset.RBACName(s.ns)); err != nil {
   207  		return nil, err
   208  	}
   209  	updated, err := s.updateAppSet(existing, appset, ctx, true)
   210  	if err != nil {
   211  		return nil, fmt.Errorf("error updating ApplicationSets: %w", err)
   212  	}
   213  	return updated, nil
   214  }
   215  
   216  func (s *Server) updateAppSet(appset *v1alpha1.ApplicationSet, newAppset *v1alpha1.ApplicationSet, ctx context.Context, merge bool) (*v1alpha1.ApplicationSet, error) {
   217  
   218  	if appset != nil && appset.Spec.Template.Spec.Project != newAppset.Spec.Template.Spec.Project {
   219  		// When changing projects, caller must have applicationset create and update privileges in new project
   220  		// NOTE: the update check was already verified in the caller to this function
   221  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionCreate, newAppset.RBACName(s.ns)); err != nil {
   222  			return nil, err
   223  		}
   224  		// They also need 'update' privileges in the old project
   225  		if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionUpdate, appset.RBACName(s.ns)); err != nil {
   226  			return nil, err
   227  		}
   228  	}
   229  
   230  	for i := 0; i < 10; i++ {
   231  		appset.Spec = newAppset.Spec
   232  		if merge {
   233  			appset.Labels = collections.MergeStringMaps(appset.Labels, newAppset.Labels)
   234  			appset.Annotations = collections.MergeStringMaps(appset.Annotations, newAppset.Annotations)
   235  		} else {
   236  			appset.Labels = newAppset.Labels
   237  			appset.Annotations = newAppset.Annotations
   238  		}
   239  
   240  		res, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(s.ns).Update(ctx, appset, metav1.UpdateOptions{})
   241  		if err == nil {
   242  			s.logAppSetEvent(appset, ctx, argo.EventReasonResourceUpdated, "updated ApplicationSets spec")
   243  			s.waitSync(res)
   244  			return res, nil
   245  		}
   246  		if !apierr.IsConflict(err) {
   247  			return nil, err
   248  		}
   249  
   250  		appset, err = s.appclientset.ArgoprojV1alpha1().ApplicationSets(s.ns).Get(ctx, newAppset.Name, metav1.GetOptions{})
   251  		if err != nil {
   252  			return nil, fmt.Errorf("error getting ApplicationSets: %w", err)
   253  		}
   254  	}
   255  	return nil, status.Errorf(codes.Internal, "Failed to update ApplicationSets. Too many conflicts")
   256  }
   257  
   258  func (s *Server) Delete(ctx context.Context, q *applicationset.ApplicationSetDeleteRequest) (*applicationset.ApplicationSetResponse, error) {
   259  
   260  	namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace)
   261  
   262  	appset, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, q.Name, metav1.GetOptions{})
   263  	if err != nil {
   264  		return nil, fmt.Errorf("error getting ApplicationSets: %w", err)
   265  	}
   266  
   267  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionDelete, appset.RBACName(s.ns)); err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	s.projectLock.RLock(appset.Spec.Template.Spec.Project)
   272  	defer s.projectLock.RUnlock(appset.Spec.Template.Spec.Project)
   273  
   274  	err = s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Delete(ctx, q.Name, metav1.DeleteOptions{})
   275  	if err != nil {
   276  		return nil, fmt.Errorf("error deleting ApplicationSets: %w", err)
   277  	}
   278  	s.logAppSetEvent(appset, ctx, argo.EventReasonResourceDeleted, "deleted ApplicationSets")
   279  	return &applicationset.ApplicationSetResponse{}, nil
   280  
   281  }
   282  
   283  func (s *Server) validateAppSet(ctx context.Context, appset *v1alpha1.ApplicationSet) (string, error) {
   284  	if appset == nil {
   285  		return "", fmt.Errorf("ApplicationSet cannot be validated for nil value")
   286  	}
   287  
   288  	projectName := appset.Spec.Template.Spec.Project
   289  
   290  	if strings.Contains(projectName, "{{") {
   291  		return "", fmt.Errorf("the Argo CD API does not currently support creating ApplicationSets with templated `project` fields")
   292  	}
   293  
   294  	if err := appsetutils.CheckInvalidGenerators(appset); err != nil {
   295  		return "", err
   296  	}
   297  
   298  	return projectName, nil
   299  }
   300  
   301  func (s *Server) checkCreatePermissions(ctx context.Context, appset *v1alpha1.ApplicationSet, projectName string) error {
   302  
   303  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplicationSets, rbacpolicy.ActionCreate, appset.RBACName(s.ns)); err != nil {
   304  		return err
   305  	}
   306  
   307  	_, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, projectName, metav1.GetOptions{})
   308  	if err != nil {
   309  		if apierr.IsNotFound(err) {
   310  			return status.Errorf(codes.InvalidArgument, "ApplicationSet references project %s which does not exist", projectName)
   311  		}
   312  		return fmt.Errorf("error getting ApplicationSet's project %q: %w", projectName, err)
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  var informerSyncTimeout = 2 * time.Second
   319  
   320  // waitSync is a helper to wait until the application informer cache is synced after create/update.
   321  // It waits until the app in the informer, has a resource version greater than the version in the
   322  // supplied app, or after 2 seconds, whichever comes first. Returns true if synced.
   323  // We use an informer cache for read operations (Get, List). Since the cache is only
   324  // eventually consistent, it is possible that it doesn't reflect an application change immediately
   325  // after a mutating API call (create/update). This function should be called after a creates &
   326  // update to give a probable (but not guaranteed) chance of being up-to-date after the create/update.
   327  func (s *Server) waitSync(appset *v1alpha1.ApplicationSet) {
   328  	logCtx := log.WithField("applicationset", appset.Name)
   329  	deadline := time.Now().Add(informerSyncTimeout)
   330  	minVersion, err := strconv.Atoi(appset.ResourceVersion)
   331  	if err != nil {
   332  		logCtx.Warnf("waitSync failed: could not parse resource version %s", appset.ResourceVersion)
   333  		time.Sleep(50 * time.Millisecond) // sleep anyway
   334  		return
   335  	}
   336  	for {
   337  		if currAppset, err := s.appsetLister.ApplicationSets(appset.Namespace).Get(appset.Name); err == nil {
   338  			currVersion, err := strconv.Atoi(currAppset.ResourceVersion)
   339  			if err == nil && currVersion >= minVersion {
   340  				return
   341  			}
   342  		}
   343  		if time.Now().After(deadline) {
   344  			break
   345  		}
   346  		time.Sleep(20 * time.Millisecond)
   347  	}
   348  	logCtx.Warnf("waitSync failed: timed out")
   349  }
   350  
   351  func (s *Server) logAppSetEvent(a *v1alpha1.ApplicationSet, ctx context.Context, reason string, action string) {
   352  	eventInfo := argo.EventInfo{Type: v1.EventTypeNormal, Reason: reason}
   353  	user := session.Username(ctx)
   354  	if user == "" {
   355  		user = "Unknown user"
   356  	}
   357  	message := fmt.Sprintf("%s %s", user, action)
   358  	s.auditLogger.LogAppSetEvent(a, eventInfo, message, user)
   359  }
   360  
   361  func (s *Server) appsetNamespaceOrDefault(appNs string) string {
   362  	if appNs == "" {
   363  		return s.ns
   364  	} else {
   365  		return appNs
   366  	}
   367  }
   368  
   369  func (s *Server) isNamespaceEnabled(namespace string) bool {
   370  	return security.IsNamespaceEnabled(namespace, s.ns, s.enabledNamespaces)
   371  }