github.com/argoproj/argo-cd/v3@v3.2.1/server/applicationset/applicationset.go (about) 1 package applicationset 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "reflect" 9 "sort" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/argoproj/pkg/v2/sync" 15 log "github.com/sirupsen/logrus" 16 "google.golang.org/grpc/codes" 17 "google.golang.org/grpc/status" 18 corev1 "k8s.io/api/core/v1" 19 apierrors "k8s.io/apimachinery/pkg/api/errors" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 "k8s.io/apimachinery/pkg/labels" 22 "k8s.io/client-go/dynamic" 23 "k8s.io/client-go/kubernetes" 24 "k8s.io/client-go/tools/cache" 25 "sigs.k8s.io/controller-runtime/pkg/client" 26 27 appsettemplate "github.com/argoproj/argo-cd/v3/applicationset/controllers/template" 28 "github.com/argoproj/argo-cd/v3/applicationset/generators" 29 "github.com/argoproj/argo-cd/v3/applicationset/services" 30 appsetstatus "github.com/argoproj/argo-cd/v3/applicationset/status" 31 appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils" 32 "github.com/argoproj/argo-cd/v3/pkg/apiclient/applicationset" 33 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 34 appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned" 35 applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1" 36 repoapiclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 37 "github.com/argoproj/argo-cd/v3/util/argo" 38 "github.com/argoproj/argo-cd/v3/util/collections" 39 "github.com/argoproj/argo-cd/v3/util/db" 40 "github.com/argoproj/argo-cd/v3/util/github_app" 41 "github.com/argoproj/argo-cd/v3/util/rbac" 42 "github.com/argoproj/argo-cd/v3/util/security" 43 "github.com/argoproj/argo-cd/v3/util/session" 44 ) 45 46 type Server struct { 47 ns string 48 db db.ArgoDB 49 enf *rbac.Enforcer 50 k8sClient kubernetes.Interface 51 dynamicClient dynamic.Interface 52 client client.Client 53 repoClientSet repoapiclient.Clientset 54 appclientset appclientset.Interface 55 appsetInformer cache.SharedIndexInformer 56 appsetLister applisters.ApplicationSetLister 57 auditLogger *argo.AuditLogger 58 projectLock sync.KeyLock 59 enabledNamespaces []string 60 GitSubmoduleEnabled bool 61 EnableNewGitFileGlobbing bool 62 ScmRootCAPath string 63 AllowedScmProviders []string 64 EnableScmProviders bool 65 EnableGitHubAPIMetrics bool 66 } 67 68 // NewServer returns a new instance of the ApplicationSet service 69 func NewServer( 70 db db.ArgoDB, 71 kubeclientset kubernetes.Interface, 72 dynamicClientset dynamic.Interface, 73 kubeControllerClientset client.Client, 74 enf *rbac.Enforcer, 75 repoClientSet repoapiclient.Clientset, 76 appclientset appclientset.Interface, 77 appsetInformer cache.SharedIndexInformer, 78 appsetLister applisters.ApplicationSetLister, 79 namespace string, 80 projectLock sync.KeyLock, 81 enabledNamespaces []string, 82 gitSubmoduleEnabled bool, 83 enableNewGitFileGlobbing bool, 84 scmRootCAPath string, 85 allowedScmProviders []string, 86 enableScmProviders bool, 87 enableGitHubAPIMetrics bool, 88 enableK8sEvent []string, 89 ) applicationset.ApplicationSetServiceServer { 90 s := &Server{ 91 ns: namespace, 92 db: db, 93 enf: enf, 94 dynamicClient: dynamicClientset, 95 client: kubeControllerClientset, 96 k8sClient: kubeclientset, 97 repoClientSet: repoClientSet, 98 appclientset: appclientset, 99 appsetInformer: appsetInformer, 100 appsetLister: appsetLister, 101 projectLock: projectLock, 102 auditLogger: argo.NewAuditLogger(kubeclientset, "argocd-server", enableK8sEvent), 103 enabledNamespaces: enabledNamespaces, 104 GitSubmoduleEnabled: gitSubmoduleEnabled, 105 EnableNewGitFileGlobbing: enableNewGitFileGlobbing, 106 ScmRootCAPath: scmRootCAPath, 107 AllowedScmProviders: allowedScmProviders, 108 EnableScmProviders: enableScmProviders, 109 EnableGitHubAPIMetrics: enableGitHubAPIMetrics, 110 } 111 return s 112 } 113 114 func (s *Server) Get(ctx context.Context, q *applicationset.ApplicationSetGetQuery) (*v1alpha1.ApplicationSet, error) { 115 namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace) 116 117 if !s.isNamespaceEnabled(namespace) { 118 return nil, security.NamespaceNotPermittedError(namespace) 119 } 120 121 a, err := s.appsetLister.ApplicationSets(namespace).Get(q.Name) 122 if err != nil { 123 return nil, fmt.Errorf("error getting ApplicationSet: %w", err) 124 } 125 err = s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionGet, a.RBACName(s.ns)) 126 if err != nil { 127 return nil, err 128 } 129 130 return a, nil 131 } 132 133 // List returns list of ApplicationSets 134 func (s *Server) List(ctx context.Context, q *applicationset.ApplicationSetListQuery) (*v1alpha1.ApplicationSetList, error) { 135 selector, err := labels.Parse(q.GetSelector()) 136 if err != nil { 137 return nil, fmt.Errorf("error parsing the selector: %w", err) 138 } 139 140 var appsets []*v1alpha1.ApplicationSet 141 if q.AppsetNamespace == "" { 142 appsets, err = s.appsetLister.List(selector) 143 } else { 144 appsets, err = s.appsetLister.ApplicationSets(q.AppsetNamespace).List(selector) 145 } 146 147 if err != nil { 148 return nil, fmt.Errorf("error listing ApplicationSets with selectors: %w", err) 149 } 150 151 newItems := make([]v1alpha1.ApplicationSet, 0) 152 for _, a := range appsets { 153 // Skip any application that is neither in the conrol plane's namespace 154 // nor in the list of enabled namespaces. 155 if !security.IsNamespaceEnabled(a.Namespace, s.ns, s.enabledNamespaces) { 156 continue 157 } 158 159 if s.enf.Enforce(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionGet, a.RBACName(s.ns)) { 160 newItems = append(newItems, *a) 161 } 162 } 163 164 newItems = argo.FilterAppSetsByProjects(newItems, q.Projects) 165 166 // Sort found applicationsets by name 167 sort.Slice(newItems, func(i, j int) bool { 168 return newItems[i].Name < newItems[j].Name 169 }) 170 171 appsetList := &v1alpha1.ApplicationSetList{ 172 ListMeta: metav1.ListMeta{ 173 ResourceVersion: s.appsetInformer.LastSyncResourceVersion(), 174 }, 175 Items: newItems, 176 } 177 return appsetList, nil 178 } 179 180 func (s *Server) Create(ctx context.Context, q *applicationset.ApplicationSetCreateRequest) (*v1alpha1.ApplicationSet, error) { 181 appset := q.GetApplicationset() 182 183 if appset == nil { 184 return nil, errors.New("error creating ApplicationSets: ApplicationSets is nil in request") 185 } 186 187 projectName, err := s.validateAppSet(appset) 188 if err != nil { 189 return nil, fmt.Errorf("error validating ApplicationSets: %w", err) 190 } 191 192 namespace := s.appsetNamespaceOrDefault(appset.Namespace) 193 194 if !s.isNamespaceEnabled(namespace) { 195 return nil, security.NamespaceNotPermittedError(namespace) 196 } 197 198 if err := s.checkCreatePermissions(ctx, appset, projectName); err != nil { 199 return nil, fmt.Errorf("error checking create permissions for ApplicationSets %s : %w", appset.Name, err) 200 } 201 202 if q.GetDryRun() { 203 apps, err := s.generateApplicationSetApps(ctx, log.WithField("applicationset", appset.Name), *appset) 204 if err != nil { 205 return nil, fmt.Errorf("unable to generate Applications of ApplicationSet: %w", err) 206 } 207 208 statusMap := appsetstatus.GetResourceStatusMap(appset) 209 statusMap = appsetstatus.BuildResourceStatus(statusMap, apps) 210 211 statuses := []v1alpha1.ResourceStatus{} 212 for _, status := range statusMap { 213 statuses = append(statuses, status) 214 } 215 appset.Status.Resources = statuses 216 return appset, nil 217 } 218 219 s.projectLock.RLock(projectName) 220 defer s.projectLock.RUnlock(projectName) 221 222 created, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Create(ctx, appset, metav1.CreateOptions{}) 223 if err == nil { 224 s.logAppSetEvent(ctx, created, argo.EventReasonResourceCreated, "created ApplicationSet") 225 s.waitSync(created) 226 return created, nil 227 } 228 229 if !apierrors.IsAlreadyExists(err) { 230 return nil, fmt.Errorf("error creating ApplicationSet: %w", err) 231 } 232 // act idempotent if existing spec matches new spec 233 existing, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, appset.Name, metav1.GetOptions{ 234 ResourceVersion: "", 235 }) 236 if err != nil { 237 return nil, status.Errorf(codes.Internal, "unable to check existing ApplicationSet details: %v", err) 238 } 239 240 equalSpecs := reflect.DeepEqual(existing.Spec, appset.Spec) && 241 reflect.DeepEqual(existing.Labels, appset.Labels) && 242 reflect.DeepEqual(existing.Annotations, appset.Annotations) && 243 reflect.DeepEqual(existing.Finalizers, appset.Finalizers) 244 245 if equalSpecs { 246 return existing, nil 247 } 248 249 if !q.Upsert { 250 return nil, status.Errorf(codes.InvalidArgument, "existing ApplicationSet spec is different, use upsert flag to force update") 251 } 252 err = s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionUpdate, appset.RBACName(s.ns)) 253 if err != nil { 254 return nil, err 255 } 256 updated, err := s.updateAppSet(ctx, existing, appset, true) 257 if err != nil { 258 return nil, fmt.Errorf("error updating ApplicationSets: %w", err) 259 } 260 return updated, nil 261 } 262 263 func (s *Server) generateApplicationSetApps(ctx context.Context, logEntry *log.Entry, appset v1alpha1.ApplicationSet) ([]v1alpha1.Application, error) { 264 argoCDDB := s.db 265 266 scmConfig := generators.NewSCMConfig(s.ScmRootCAPath, s.AllowedScmProviders, s.EnableScmProviders, s.EnableGitHubAPIMetrics, github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB)), true) 267 argoCDService := services.NewArgoCDService(s.db, s.GitSubmoduleEnabled, s.repoClientSet, s.EnableNewGitFileGlobbing) 268 appSetGenerators := generators.GetGenerators(ctx, s.client, s.k8sClient, s.ns, argoCDService, s.dynamicClient, scmConfig) 269 270 apps, _, err := appsettemplate.GenerateApplications(logEntry, appset, appSetGenerators, &appsetutils.Render{}, s.client) 271 if err != nil { 272 return nil, fmt.Errorf("error generating applications: %w", err) 273 } 274 return apps, nil 275 } 276 277 func (s *Server) updateAppSet(ctx context.Context, appset *v1alpha1.ApplicationSet, newAppset *v1alpha1.ApplicationSet, merge bool) (*v1alpha1.ApplicationSet, error) { 278 if appset != nil && appset.Spec.Template.Spec.Project != newAppset.Spec.Template.Spec.Project { 279 // When changing projects, caller must have applicationset create and update privileges in new project 280 // NOTE: the update check was already verified in the caller to this function 281 if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionCreate, newAppset.RBACName(s.ns)); err != nil { 282 return nil, err 283 } 284 // They also need 'update' privileges in the old project 285 if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionUpdate, appset.RBACName(s.ns)); err != nil { 286 return nil, err 287 } 288 } 289 290 for i := 0; i < 10; i++ { 291 appset.Spec = newAppset.Spec 292 if merge { 293 appset.Labels = collections.Merge(appset.Labels, newAppset.Labels) 294 appset.Annotations = collections.Merge(appset.Annotations, newAppset.Annotations) 295 } else { 296 appset.Labels = newAppset.Labels 297 appset.Annotations = newAppset.Annotations 298 } 299 appset.Finalizers = newAppset.Finalizers 300 res, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(appset.Namespace).Update(ctx, appset, metav1.UpdateOptions{}) 301 if err == nil { 302 s.logAppSetEvent(ctx, appset, argo.EventReasonResourceUpdated, "updated ApplicationSets spec") 303 s.waitSync(res) 304 return res, nil 305 } 306 if !apierrors.IsConflict(err) { 307 return nil, err 308 } 309 310 appset, err = s.appclientset.ArgoprojV1alpha1().ApplicationSets(appset.Namespace).Get(ctx, appset.Name, metav1.GetOptions{}) 311 if err != nil { 312 return nil, fmt.Errorf("error getting ApplicationSets: %w", err) 313 } 314 } 315 return nil, status.Errorf(codes.Internal, "Failed to update ApplicationSets. Too many conflicts") 316 } 317 318 func (s *Server) Delete(ctx context.Context, q *applicationset.ApplicationSetDeleteRequest) (*applicationset.ApplicationSetResponse, error) { 319 namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace) 320 321 appset, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, q.Name, metav1.GetOptions{}) 322 if err != nil { 323 return nil, fmt.Errorf("error getting ApplicationSets: %w", err) 324 } 325 326 if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionDelete, appset.RBACName(s.ns)); err != nil { 327 return nil, err 328 } 329 330 s.projectLock.RLock(appset.Spec.Template.Spec.Project) 331 defer s.projectLock.RUnlock(appset.Spec.Template.Spec.Project) 332 333 err = s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Delete(ctx, q.Name, metav1.DeleteOptions{}) 334 if err != nil { 335 return nil, fmt.Errorf("error deleting ApplicationSets: %w", err) 336 } 337 s.logAppSetEvent(ctx, appset, argo.EventReasonResourceDeleted, "deleted ApplicationSets") 338 return &applicationset.ApplicationSetResponse{}, nil 339 } 340 341 func (s *Server) ResourceTree(ctx context.Context, q *applicationset.ApplicationSetTreeQuery) (*v1alpha1.ApplicationSetTree, error) { 342 namespace := s.appsetNamespaceOrDefault(q.AppsetNamespace) 343 344 if !s.isNamespaceEnabled(namespace) { 345 return nil, security.NamespaceNotPermittedError(namespace) 346 } 347 348 a, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(ctx, q.Name, metav1.GetOptions{}) 349 if err != nil { 350 return nil, fmt.Errorf("error getting ApplicationSet: %w", err) 351 } 352 err = s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionGet, a.RBACName(s.ns)) 353 if err != nil { 354 return nil, err 355 } 356 357 return s.buildApplicationSetTree(a) 358 } 359 360 func (s *Server) Generate(ctx context.Context, q *applicationset.ApplicationSetGenerateRequest) (*applicationset.ApplicationSetGenerateResponse, error) { 361 appset := q.GetApplicationSet() 362 363 if appset == nil { 364 return nil, errors.New("error creating ApplicationSets: ApplicationSets is nil in request") 365 } 366 367 // The RBAC check needs to be performed against the appset namespace 368 // However, when trying to generate params, the server namespace needs 369 // to be passed. 370 namespace := s.appsetNamespaceOrDefault(appset.Namespace) 371 if !s.isNamespaceEnabled(namespace) { 372 return nil, security.NamespaceNotPermittedError(namespace) 373 } 374 375 projectName, err := s.validateAppSet(appset) 376 if err != nil { 377 return nil, fmt.Errorf("error validating ApplicationSets: %w", err) 378 } 379 if err := s.checkCreatePermissions(ctx, appset, projectName); err != nil { 380 return nil, fmt.Errorf("error checking create permissions for ApplicationSets %s : %w", appset.Name, err) 381 } 382 383 logs := bytes.NewBuffer(nil) 384 logger := log.New() 385 logger.SetOutput(logs) 386 387 // The server namespace will be used in the function 388 // since this is the exact namespace that is being used 389 // to generate parameters (especially for git generator). 390 // 391 // In case of Git generator, if the namespace is set to 392 // appset namespace, we'll look for a project in the appset 393 // namespace that would lead to error when generating params 394 // for an appset in any namespace feature. 395 // See https://github.com/argoproj/argo-cd/issues/22942 396 apps, err := s.generateApplicationSetApps(ctx, logger.WithField("applicationset", appset.Name), *appset) 397 if err != nil { 398 return nil, fmt.Errorf("unable to generate Applications of ApplicationSet: %w\n%s", err, logs.String()) 399 } 400 res := &applicationset.ApplicationSetGenerateResponse{} 401 for i := range apps { 402 res.Applications = append(res.Applications, &apps[i]) 403 } 404 return res, nil 405 } 406 407 func (s *Server) buildApplicationSetTree(a *v1alpha1.ApplicationSet) (*v1alpha1.ApplicationSetTree, error) { 408 var tree v1alpha1.ApplicationSetTree 409 410 gvk := v1alpha1.ApplicationSetSchemaGroupVersionKind 411 parentRefs := []v1alpha1.ResourceRef{ 412 {Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind, Name: a.Name, Namespace: a.Namespace, UID: string(a.UID)}, 413 } 414 415 apps := a.Status.Resources 416 for _, app := range apps { 417 tree.Nodes = append(tree.Nodes, v1alpha1.ResourceNode{ 418 Health: app.Health, 419 ResourceRef: v1alpha1.ResourceRef{ 420 Name: app.Name, 421 Group: app.Group, 422 Version: app.Version, 423 Kind: app.Kind, 424 Namespace: a.Namespace, 425 }, 426 ParentRefs: parentRefs, 427 }) 428 } 429 tree.Normalize() 430 431 return &tree, nil 432 } 433 434 func (s *Server) validateAppSet(appset *v1alpha1.ApplicationSet) (string, error) { 435 if appset == nil { 436 return "", errors.New("ApplicationSet cannot be validated for nil value") 437 } 438 439 projectName := appset.Spec.Template.Spec.Project 440 441 if strings.Contains(projectName, "{{") { 442 return "", errors.New("the Argo CD API does not currently support creating ApplicationSets with templated `project` fields") 443 } 444 445 if err := appsetutils.CheckInvalidGenerators(appset); err != nil { 446 return "", err 447 } 448 449 return projectName, nil 450 } 451 452 func (s *Server) checkCreatePermissions(ctx context.Context, appset *v1alpha1.ApplicationSet, projectName string) error { 453 if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplicationSets, rbac.ActionCreate, appset.RBACName(s.ns)); err != nil { 454 return err 455 } 456 457 _, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(ctx, projectName, metav1.GetOptions{}) 458 if err != nil { 459 if apierrors.IsNotFound(err) { 460 return status.Errorf(codes.InvalidArgument, "ApplicationSet references project %s which does not exist", projectName) 461 } 462 return fmt.Errorf("error getting ApplicationSet's project %q: %w", projectName, err) 463 } 464 465 return nil 466 } 467 468 var informerSyncTimeout = 2 * time.Second 469 470 // waitSync is a helper to wait until the application informer cache is synced after create/update. 471 // It waits until the app in the informer, has a resource version greater than the version in the 472 // supplied app, or after 2 seconds, whichever comes first. Returns true if synced. 473 // We use an informer cache for read operations (Get, List). Since the cache is only 474 // eventually consistent, it is possible that it doesn't reflect an application change immediately 475 // after a mutating API call (create/update). This function should be called after a creates & 476 // update to give a probable (but not guaranteed) chance of being up-to-date after the create/update. 477 func (s *Server) waitSync(appset *v1alpha1.ApplicationSet) { 478 logCtx := log.WithField("applicationset", appset.Name) 479 deadline := time.Now().Add(informerSyncTimeout) 480 minVersion, err := strconv.Atoi(appset.ResourceVersion) 481 if err != nil { 482 logCtx.Warnf("waitSync failed: could not parse resource version %s", appset.ResourceVersion) 483 time.Sleep(50 * time.Millisecond) // sleep anyway 484 return 485 } 486 for { 487 if currAppset, err := s.appsetLister.ApplicationSets(appset.Namespace).Get(appset.Name); err == nil { 488 currVersion, err := strconv.Atoi(currAppset.ResourceVersion) 489 if err == nil && currVersion >= minVersion { 490 return 491 } 492 } 493 if time.Now().After(deadline) { 494 break 495 } 496 time.Sleep(20 * time.Millisecond) 497 } 498 logCtx.Warnf("waitSync failed: timed out") 499 } 500 501 func (s *Server) logAppSetEvent(ctx context.Context, a *v1alpha1.ApplicationSet, reason string, action string) { 502 eventInfo := argo.EventInfo{Type: corev1.EventTypeNormal, Reason: reason} 503 user := session.Username(ctx) 504 if user == "" { 505 user = "Unknown user" 506 } 507 message := fmt.Sprintf("%s %s", user, action) 508 s.auditLogger.LogAppSetEvent(a, eventInfo, message, user) 509 } 510 511 func (s *Server) appsetNamespaceOrDefault(appNs string) string { 512 if appNs == "" { 513 return s.ns 514 } 515 return appNs 516 } 517 518 func (s *Server) isNamespaceEnabled(namespace string) bool { 519 return security.IsNamespaceEnabled(namespace, s.ns, s.enabledNamespaces) 520 }