github.com/argoproj/argo-cd@v1.8.7/util/argo/argo.go (about) 1 package argo 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/argoproj/gitops-engine/pkg/utils/kube" 11 log "github.com/sirupsen/logrus" 12 "google.golang.org/grpc/codes" 13 "google.golang.org/grpc/status" 14 apierr "k8s.io/apimachinery/pkg/api/errors" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/fields" 17 "k8s.io/apimachinery/pkg/runtime/schema" 18 "k8s.io/apimachinery/pkg/types" 19 "k8s.io/apimachinery/pkg/watch" 20 21 "github.com/argoproj/argo-cd/common" 22 argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 23 "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1" 24 applicationsv1 "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1" 25 "github.com/argoproj/argo-cd/reposerver/apiclient" 26 "github.com/argoproj/argo-cd/util/db" 27 "github.com/argoproj/argo-cd/util/git" 28 "github.com/argoproj/argo-cd/util/helm" 29 "github.com/argoproj/argo-cd/util/io" 30 "github.com/argoproj/argo-cd/util/settings" 31 ) 32 33 const ( 34 errDestinationMissing = "Destination server missing from app spec" 35 ) 36 37 // FormatAppConditions returns string representation of give app condition list 38 func FormatAppConditions(conditions []argoappv1.ApplicationCondition) string { 39 formattedConditions := make([]string, 0) 40 for _, condition := range conditions { 41 formattedConditions = append(formattedConditions, fmt.Sprintf("%s: %s", condition.Type, condition.Message)) 42 } 43 return strings.Join(formattedConditions, ";") 44 } 45 46 // FilterByProjects returns applications which belongs to the specified project 47 func FilterByProjects(apps []argoappv1.Application, projects []string) []argoappv1.Application { 48 if len(projects) == 0 { 49 return apps 50 } 51 projectsMap := make(map[string]bool) 52 for i := range projects { 53 projectsMap[projects[i]] = true 54 } 55 items := make([]argoappv1.Application, 0) 56 for i := 0; i < len(apps); i++ { 57 a := apps[i] 58 if _, ok := projectsMap[a.Spec.GetProject()]; ok { 59 items = append(items, a) 60 } 61 } 62 return items 63 64 } 65 66 // RefreshApp updates the refresh annotation of an application to coerce the controller to process it 67 func RefreshApp(appIf v1alpha1.ApplicationInterface, name string, refreshType argoappv1.RefreshType) (*argoappv1.Application, error) { 68 metadata := map[string]interface{}{ 69 "metadata": map[string]interface{}{ 70 "annotations": map[string]string{ 71 common.AnnotationKeyRefresh: string(refreshType), 72 }, 73 }, 74 } 75 var err error 76 patch, err := json.Marshal(metadata) 77 if err != nil { 78 return nil, err 79 } 80 for attempt := 0; attempt < 5; attempt++ { 81 app, err := appIf.Patch(context.Background(), name, types.MergePatchType, patch, metav1.PatchOptions{}) 82 if err != nil { 83 if !apierr.IsConflict(err) { 84 return nil, err 85 } 86 } else { 87 log.Infof("Requested app '%s' refresh", name) 88 return app, nil 89 } 90 time.Sleep(100 * time.Millisecond) 91 } 92 return nil, err 93 } 94 95 // WaitForRefresh watches an application until its comparison timestamp is after the refresh timestamp 96 // If refresh timestamp is not present, will use current timestamp at time of call 97 func WaitForRefresh(ctx context.Context, appIf v1alpha1.ApplicationInterface, name string, timeout *time.Duration) (*argoappv1.Application, error) { 98 var cancel context.CancelFunc 99 if timeout != nil { 100 ctx, cancel = context.WithTimeout(ctx, *timeout) 101 defer cancel() 102 } 103 ch := kube.WatchWithRetry(ctx, func() (i watch.Interface, e error) { 104 fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", name)) 105 listOpts := metav1.ListOptions{FieldSelector: fieldSelector.String()} 106 return appIf.Watch(ctx, listOpts) 107 }) 108 for next := range ch { 109 if next.Error != nil { 110 return nil, next.Error 111 } 112 app, ok := next.Object.(*argoappv1.Application) 113 if !ok { 114 return nil, fmt.Errorf("Application event object failed conversion: %v", next) 115 } 116 annotations := app.GetAnnotations() 117 if annotations == nil { 118 annotations = make(map[string]string) 119 } 120 if _, ok := annotations[common.AnnotationKeyRefresh]; !ok { 121 return app, nil 122 } 123 } 124 return nil, fmt.Errorf("application refresh deadline exceeded") 125 } 126 127 func TestRepoWithKnownType(repo *argoappv1.Repository, isHelm bool, isHelmOci bool) error { 128 repo = repo.DeepCopy() 129 if isHelm { 130 repo.Type = "helm" 131 } else { 132 repo.Type = "git" 133 } 134 repo.EnableOCI = repo.EnableOCI || isHelmOci 135 136 return TestRepo(repo) 137 } 138 139 func TestRepo(repo *argoappv1.Repository) error { 140 checks := map[string]func() error{ 141 "git": func() error { 142 return git.TestRepo(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled()) 143 }, 144 "helm": func() error { 145 if repo.EnableOCI { 146 _, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI).TestHelmOCI() 147 return err 148 } else { 149 _, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI).GetIndex() 150 return err 151 } 152 }, 153 } 154 if check, ok := checks[repo.Type]; ok { 155 return check() 156 } 157 var err error 158 for _, check := range checks { 159 err = check() 160 if err == nil { 161 return nil 162 } 163 } 164 return err 165 } 166 167 // ValidateRepo validates the repository specified in application spec. Following is checked: 168 // * the repository is accessible 169 // * the path contains valid manifests 170 // * there are parameters of only one app source type 171 // * ksonnet: the specified environment exists 172 func ValidateRepo( 173 ctx context.Context, 174 app *argoappv1.Application, 175 repoClientset apiclient.Clientset, 176 db db.ArgoDB, 177 kustomizeOptions *argoappv1.KustomizeOptions, 178 plugins []*argoappv1.ConfigManagementPlugin, 179 kubectl kube.Kubectl, 180 ) ([]argoappv1.ApplicationCondition, error) { 181 spec := &app.Spec 182 conditions := make([]argoappv1.ApplicationCondition, 0) 183 184 // Test the repo 185 conn, repoClient, err := repoClientset.NewRepoServerClient() 186 if err != nil { 187 return nil, err 188 } 189 defer io.Close(conn) 190 repo, err := db.GetRepository(ctx, spec.Source.RepoURL) 191 if err != nil { 192 return nil, err 193 } 194 195 repoAccessible := false 196 err = TestRepoWithKnownType(repo, app.Spec.Source.IsHelm(), app.Spec.Source.IsHelmOci()) 197 if err != nil { 198 conditions = append(conditions, argoappv1.ApplicationCondition{ 199 Type: argoappv1.ApplicationConditionInvalidSpecError, 200 Message: fmt.Sprintf("repository not accessible: %v", err), 201 }) 202 } else { 203 repoAccessible = true 204 } 205 206 // Verify only one source type is defined 207 _, err = spec.Source.ExplicitType() 208 if err != nil { 209 return nil, err 210 } 211 212 // is the repo inaccessible - abort now 213 if !repoAccessible { 214 return conditions, nil 215 } 216 217 helmRepos, err := db.ListHelmRepositories(ctx) 218 if err != nil { 219 return nil, err 220 } 221 222 // get the app details, and populate the Ksonnet stuff from it 223 appDetails, err := repoClient.GetAppDetails(ctx, &apiclient.RepoServerAppDetailsQuery{ 224 Repo: repo, 225 Source: &spec.Source, 226 Repos: helmRepos, 227 KustomizeOptions: kustomizeOptions, 228 }) 229 if err != nil { 230 conditions = append(conditions, argoappv1.ApplicationCondition{ 231 Type: argoappv1.ApplicationConditionInvalidSpecError, 232 Message: fmt.Sprintf("Unable to get app details: %v", err), 233 }) 234 return conditions, nil 235 } 236 237 enrichSpec(spec, appDetails) 238 239 cluster, err := db.GetCluster(context.Background(), spec.Destination.Server) 240 if err != nil { 241 conditions = append(conditions, argoappv1.ApplicationCondition{ 242 Type: argoappv1.ApplicationConditionInvalidSpecError, 243 Message: fmt.Sprintf("Unable to get cluster: %v", err), 244 }) 245 return conditions, nil 246 } 247 config := cluster.RESTConfig() 248 cluster.ServerVersion, err = kubectl.GetServerVersion(config) 249 if err != nil { 250 return nil, err 251 } 252 apiGroups, err := kubectl.GetAPIGroups(config) 253 if err != nil { 254 return nil, err 255 } 256 conditions = append(conditions, verifyGenerateManifests( 257 ctx, repo, helmRepos, app, repoClient, kustomizeOptions, plugins, cluster.ServerVersion, APIGroupsToVersions(apiGroups))...) 258 259 return conditions, nil 260 } 261 262 func enrichSpec(spec *argoappv1.ApplicationSpec, appDetails *apiclient.RepoAppDetailsResponse) { 263 if spec.Source.Ksonnet != nil && appDetails.Ksonnet != nil { 264 env, ok := appDetails.Ksonnet.Environments[spec.Source.Ksonnet.Environment] 265 if ok { 266 // If server and namespace are not supplied, pull it from the app.yaml 267 if spec.Destination.Server == "" { 268 spec.Destination.Server = env.Destination.Server 269 } 270 if spec.Destination.Namespace == "" { 271 spec.Destination.Namespace = env.Destination.Namespace 272 } 273 } 274 } 275 } 276 277 // ValidateDestination checks: 278 // if we used destination name we infer the server url 279 // if we used both name and server then we return an invalid spec error 280 func ValidateDestination(ctx context.Context, dest *argoappv1.ApplicationDestination, db db.ArgoDB) error { 281 if dest.Name != "" { 282 if dest.Server == "" { 283 server, err := getDestinationServer(ctx, db, dest.Name) 284 if err != nil { 285 return fmt.Errorf("unable to find destination server: %v", err) 286 } 287 if server == "" { 288 return fmt.Errorf("application references destination cluster %s which does not exist", dest.Name) 289 } 290 dest.SetInferredServer(server) 291 } else { 292 if !dest.IsServerInferred() { 293 return fmt.Errorf("application destination can't have both name and server defined: %s %s", dest.Name, dest.Server) 294 } 295 } 296 } 297 return nil 298 } 299 300 // ValidatePermissions ensures that the referenced cluster has been added to Argo CD and the app source repo and destination namespace/cluster are permitted in app project 301 func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, proj *argoappv1.AppProject, db db.ArgoDB) ([]argoappv1.ApplicationCondition, error) { 302 conditions := make([]argoappv1.ApplicationCondition, 0) 303 if spec.Source.RepoURL == "" || (spec.Source.Path == "" && spec.Source.Chart == "") { 304 conditions = append(conditions, argoappv1.ApplicationCondition{ 305 Type: argoappv1.ApplicationConditionInvalidSpecError, 306 Message: "spec.source.repoURL and spec.source.path either spec.source.chart are required", 307 }) 308 return conditions, nil 309 } 310 if spec.Source.Chart != "" && spec.Source.TargetRevision == "" { 311 conditions = append(conditions, argoappv1.ApplicationCondition{ 312 Type: argoappv1.ApplicationConditionInvalidSpecError, 313 Message: "spec.source.targetRevision is required if the manifest source is a helm chart", 314 }) 315 return conditions, nil 316 } 317 318 if !proj.IsSourcePermitted(spec.Source) { 319 conditions = append(conditions, argoappv1.ApplicationCondition{ 320 Type: argoappv1.ApplicationConditionInvalidSpecError, 321 Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.Source.RepoURL, spec.Project), 322 }) 323 } 324 325 if spec.Destination.Server != "" { 326 if !proj.IsDestinationPermitted(spec.Destination) { 327 conditions = append(conditions, argoappv1.ApplicationCondition{ 328 Type: argoappv1.ApplicationConditionInvalidSpecError, 329 Message: fmt.Sprintf("application destination {%s %s} is not permitted in project '%s'", spec.Destination.Server, spec.Destination.Namespace, spec.Project), 330 }) 331 } 332 // Ensure the k8s cluster the app is referencing, is configured in Argo CD 333 _, err := db.GetCluster(ctx, spec.Destination.Server) 334 if err != nil { 335 if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.NotFound { 336 conditions = append(conditions, argoappv1.ApplicationCondition{ 337 Type: argoappv1.ApplicationConditionInvalidSpecError, 338 Message: fmt.Sprintf("cluster '%s' has not been configured", spec.Destination.Server), 339 }) 340 } else { 341 return nil, err 342 } 343 } 344 } else if spec.Destination.Server == "" { 345 conditions = append(conditions, argoappv1.ApplicationCondition{Type: argoappv1.ApplicationConditionInvalidSpecError, Message: errDestinationMissing}) 346 } 347 return conditions, nil 348 } 349 350 // APIGroupsToVersions converts list of API Groups into versions string list 351 func APIGroupsToVersions(apiGroups []metav1.APIGroup) []string { 352 var apiVersions []string 353 for _, g := range apiGroups { 354 for _, v := range g.Versions { 355 apiVersions = append(apiVersions, v.GroupVersion) 356 } 357 } 358 return apiVersions 359 } 360 361 // GetAppProject returns a project from an application 362 func GetAppProject(spec *argoappv1.ApplicationSpec, projLister applicationsv1.AppProjectLister, ns string, settingsManager *settings.SettingsManager) (*argoappv1.AppProject, error) { 363 projOrig, err := projLister.AppProjects(ns).Get(spec.GetProject()) 364 if err != nil { 365 return nil, err 366 } 367 return GetAppVirtualProject(projOrig, projLister, settingsManager) 368 } 369 370 // verifyGenerateManifests verifies a repo path can generate manifests 371 func verifyGenerateManifests( 372 ctx context.Context, 373 repoRes *argoappv1.Repository, 374 helmRepos argoappv1.Repositories, 375 app *argoappv1.Application, 376 repoClient apiclient.RepoServerServiceClient, 377 kustomizeOptions *argoappv1.KustomizeOptions, 378 plugins []*argoappv1.ConfigManagementPlugin, 379 kubeVersion string, 380 apiVersions []string, 381 ) []argoappv1.ApplicationCondition { 382 spec := &app.Spec 383 var conditions []argoappv1.ApplicationCondition 384 if spec.Destination.Server == "" { 385 conditions = append(conditions, argoappv1.ApplicationCondition{ 386 Type: argoappv1.ApplicationConditionInvalidSpecError, 387 Message: errDestinationMissing, 388 }) 389 } 390 391 req := apiclient.ManifestRequest{ 392 Repo: &argoappv1.Repository{ 393 Repo: spec.Source.RepoURL, 394 Type: repoRes.Type, 395 Name: repoRes.Name, 396 }, 397 Repos: helmRepos, 398 Revision: spec.Source.TargetRevision, 399 AppLabelValue: app.Name, 400 Namespace: spec.Destination.Namespace, 401 ApplicationSource: &spec.Source, 402 Plugins: plugins, 403 KustomizeOptions: kustomizeOptions, 404 KubeVersion: kubeVersion, 405 ApiVersions: apiVersions, 406 } 407 req.Repo.CopyCredentialsFromRepo(repoRes) 408 req.Repo.CopySettingsFrom(repoRes) 409 410 // Only check whether we can access the application's path, 411 // and not whether it actually contains any manifests. 412 _, err := repoClient.GenerateManifest(ctx, &req) 413 if err != nil { 414 conditions = append(conditions, argoappv1.ApplicationCondition{ 415 Type: argoappv1.ApplicationConditionInvalidSpecError, 416 Message: fmt.Sprintf("Unable to generate manifests in %s: %v", spec.Source.Path, err), 417 }) 418 } 419 420 return conditions 421 } 422 423 // SetAppOperation updates an application with the specified operation, retrying conflict errors 424 func SetAppOperation(appIf v1alpha1.ApplicationInterface, appName string, op *argoappv1.Operation) (*argoappv1.Application, error) { 425 for { 426 a, err := appIf.Get(context.Background(), appName, metav1.GetOptions{}) 427 if err != nil { 428 return nil, err 429 } 430 if a.Operation != nil { 431 return nil, status.Errorf(codes.FailedPrecondition, "another operation is already in progress") 432 } 433 a.Operation = op 434 a.Status.OperationState = nil 435 a, err = appIf.Update(context.Background(), a, metav1.UpdateOptions{}) 436 if op.Sync == nil { 437 return nil, status.Errorf(codes.InvalidArgument, "Operation unspecified") 438 } 439 if err == nil { 440 return a, nil 441 } 442 if !apierr.IsConflict(err) { 443 return nil, err 444 } 445 log.Warnf("Failed to set operation for app '%s' due to update conflict. Retrying again...", appName) 446 } 447 } 448 449 // ContainsSyncResource determines if the given resource exists in the provided slice of sync operation resources. 450 func ContainsSyncResource(name string, namespace string, gvk schema.GroupVersionKind, rr []argoappv1.SyncOperationResource) bool { 451 for _, r := range rr { 452 if r.HasIdentity(name, namespace, gvk) { 453 return true 454 } 455 } 456 return false 457 } 458 459 // NormalizeApplicationSpec will normalize an application spec to a preferred state. This is used 460 // for migrating application objects which are using deprecated legacy fields into the new fields, 461 // and defaulting fields in the spec (e.g. spec.project) 462 func NormalizeApplicationSpec(spec *argoappv1.ApplicationSpec) *argoappv1.ApplicationSpec { 463 spec = spec.DeepCopy() 464 if spec.Project == "" { 465 spec.Project = common.DefaultAppProjectName 466 } 467 468 // 3. If any app sources are their zero values, then nil out the pointers to the source spec. 469 // This makes it easier for users to switch between app source types if they are not using 470 // any of the source-specific parameters. 471 if spec.Source.Kustomize != nil && spec.Source.Kustomize.IsZero() { 472 spec.Source.Kustomize = nil 473 } 474 if spec.Source.Helm != nil && spec.Source.Helm.IsZero() { 475 spec.Source.Helm = nil 476 } 477 if spec.Source.Ksonnet != nil && spec.Source.Ksonnet.IsZero() { 478 spec.Source.Ksonnet = nil 479 } 480 if spec.Source.Directory != nil && spec.Source.Directory.IsZero() { 481 if spec.Source.Directory.Exclude != "" { 482 spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: spec.Source.Directory.Exclude} 483 } else { 484 spec.Source.Directory = nil 485 } 486 } 487 return spec 488 } 489 490 func getDestinationServer(ctx context.Context, db db.ArgoDB, clusterName string) (string, error) { 491 clusterList, err := db.ListClusters(ctx) 492 if err != nil { 493 return "", err 494 } 495 var servers []string 496 for _, c := range clusterList.Items { 497 if c.Name == clusterName { 498 servers = append(servers, c.Server) 499 } 500 } 501 if len(servers) > 1 { 502 return "", fmt.Errorf("there are %d clusters with the same name: %v", len(servers), servers) 503 } else if len(servers) == 0 { 504 return "", fmt.Errorf("there are no clusters with this name: %s", clusterName) 505 } 506 return servers[0], nil 507 } 508 509 func GetGlobalProjects(proj *argoappv1.AppProject, projLister applicationsv1.AppProjectLister, settingsManager *settings.SettingsManager) []*argoappv1.AppProject { 510 gps, err := settingsManager.GetGlobalProjectsSettings() 511 globalProjects := make([]*argoappv1.AppProject, 0) 512 513 if err != nil { 514 log.Warnf("Failed to get global project settings: %v", err) 515 return globalProjects 516 } 517 518 for _, gp := range gps { 519 //The project itself is not its own the global project 520 if proj.Name == gp.ProjectName { 521 continue 522 } 523 524 selector, err := metav1.LabelSelectorAsSelector(&gp.LabelSelector) 525 if err != nil { 526 break 527 } 528 //Get projects which match the label selector, then see if proj is a match 529 projList, err := projLister.AppProjects(proj.Namespace).List(selector) 530 if err != nil { 531 break 532 } 533 var matchMe bool 534 for _, item := range projList { 535 if item.Name == proj.Name { 536 matchMe = true 537 break 538 } 539 } 540 if !matchMe { 541 break 542 } 543 //If proj is a match for this global project setting, then it is its global project 544 globalProj, err := projLister.AppProjects(proj.Namespace).Get(gp.ProjectName) 545 if err != nil { 546 break 547 } 548 globalProjects = append(globalProjects, globalProj) 549 550 } 551 return globalProjects 552 } 553 554 func GetAppVirtualProject(proj *argoappv1.AppProject, projLister applicationsv1.AppProjectLister, settingsManager *settings.SettingsManager) (*argoappv1.AppProject, error) { 555 virtualProj := proj.DeepCopy() 556 globalProjects := GetGlobalProjects(proj, projLister, settingsManager) 557 558 for _, gp := range globalProjects { 559 virtualProj = mergeVirtualProject(virtualProj, gp) 560 } 561 return virtualProj, nil 562 } 563 564 func mergeVirtualProject(proj *argoappv1.AppProject, globalProj *argoappv1.AppProject) *argoappv1.AppProject { 565 if globalProj == nil { 566 return proj 567 } 568 proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, globalProj.Spec.ClusterResourceWhitelist...) 569 proj.Spec.ClusterResourceBlacklist = append(proj.Spec.ClusterResourceBlacklist, globalProj.Spec.ClusterResourceBlacklist...) 570 571 proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist, globalProj.Spec.NamespaceResourceWhitelist...) 572 proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, globalProj.Spec.NamespaceResourceBlacklist...) 573 574 proj.Spec.SyncWindows = append(proj.Spec.SyncWindows, globalProj.Spec.SyncWindows...) 575 576 return proj 577 }