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 }