github.com/argoproj/argo-cd/v3@v3.2.1/util/argo/argo.go (about)

     1  package argo
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"regexp"
     9  	"slices"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/argoproj/gitops-engine/pkg/cache"
    15  	"github.com/argoproj/gitops-engine/pkg/sync/common"
    16  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    17  	"github.com/r3labs/diff/v3"
    18  	log "github.com/sirupsen/logrus"
    19  	"google.golang.org/grpc/codes"
    20  	"google.golang.org/grpc/status"
    21  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    22  	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apimachinery/pkg/types"
    26  
    27  	"github.com/argoproj/argo-cd/v3/util/gpg"
    28  
    29  	argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    30  	"github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/typed/application/v1alpha1"
    31  	applicationsv1 "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
    32  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    33  	"github.com/argoproj/argo-cd/v3/util/db"
    34  	"github.com/argoproj/argo-cd/v3/util/glob"
    35  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    36  	"github.com/argoproj/argo-cd/v3/util/settings"
    37  )
    38  
    39  const (
    40  	ErrDestinationMissing = "Destination server missing from app spec"
    41  )
    42  
    43  var ErrAnotherOperationInProgress = status.Errorf(codes.FailedPrecondition, "another operation is already in progress")
    44  
    45  // AugmentSyncMsg enrich the K8s message with user-relevant information
    46  func AugmentSyncMsg(res common.ResourceSyncResult, apiResourceInfoGetter func() ([]kube.APIResourceInfo, error)) (string, error) {
    47  	if strings.Contains(res.Message, "the server could not find the requested resource") {
    48  		resource, err := getAPIResourceInfo(res.ResourceKey.Group, res.ResourceKey.Kind, apiResourceInfoGetter)
    49  		if err != nil {
    50  			return "", fmt.Errorf("failed to get API resource info for group %q and kind %q: %w", res.ResourceKey.Group, res.ResourceKey.Kind, err)
    51  		}
    52  		if resource == nil {
    53  			return fmt.Sprintf("The Kubernetes API could not find %s/%s for requested resource %s/%s. Make sure the %q CRD is installed on the destination cluster.", res.ResourceKey.Group, res.ResourceKey.Kind, res.ResourceKey.Namespace, res.ResourceKey.Name, res.ResourceKey.Kind), nil
    54  		}
    55  		return fmt.Sprintf("The Kubernetes API could not find version %q of %s/%s for requested resource %s/%s. Version %q of %s/%s is installed on the destination cluster.", res.Version, res.ResourceKey.Group, res.ResourceKey.Kind, res.ResourceKey.Namespace, res.ResourceKey.Name, resource.GroupVersionResource.Version, resource.GroupKind.Group, resource.GroupKind.Kind), nil
    56  	}
    57  	// Check if the message contains "metadata.annotation: Too long"
    58  	if strings.Contains(res.Message, "metadata.annotations: Too long: must have at most 262144 bytes") {
    59  		return res.Message + " \n -Additional Info: This error usually means that you are trying to add a large resource on client side. Consider using Server-side apply or syncing with replace enabled. Note: Syncing with Replace enabled is potentially destructive as it may cause resource deletion and re-creation.", nil
    60  	}
    61  
    62  	return res.Message, nil
    63  }
    64  
    65  // getAPIResourceInfo gets Kubernetes API resource info for the given group and kind. If there's a matching resource
    66  // group _and_ kind, it will return the resource info. If there's a matching kind but no matching group, it will
    67  // return the first resource info that matches the kind. If there's no matching kind, it will return nil.
    68  func getAPIResourceInfo(group, kind string, getAPIResourceInfo func() ([]kube.APIResourceInfo, error)) (*kube.APIResourceInfo, error) {
    69  	apiResources, err := getAPIResourceInfo()
    70  	if err != nil {
    71  		return nil, fmt.Errorf("failed to get API resource info: %w", err)
    72  	}
    73  
    74  	for _, r := range apiResources {
    75  		if r.GroupKind.Group == group && r.GroupKind.Kind == kind {
    76  			return &r, nil
    77  		}
    78  	}
    79  
    80  	for _, r := range apiResources {
    81  		if r.GroupKind.Kind == kind {
    82  			return &r, nil
    83  		}
    84  	}
    85  
    86  	return nil, nil
    87  }
    88  
    89  // FormatAppConditions returns string representation of give app condition list
    90  func FormatAppConditions(conditions []argoappv1.ApplicationCondition) string {
    91  	formattedConditions := make([]string, 0)
    92  	for _, condition := range conditions {
    93  		formattedConditions = append(formattedConditions, fmt.Sprintf("%s: %s", condition.Type, condition.Message))
    94  	}
    95  	return strings.Join(formattedConditions, ";")
    96  }
    97  
    98  // FilterByProjects returns applications which belongs to the specified project
    99  func FilterByProjects(apps []argoappv1.Application, projects []string) []argoappv1.Application {
   100  	if len(projects) == 0 {
   101  		return apps
   102  	}
   103  	projectsMap := make(map[string]bool)
   104  	for i := range projects {
   105  		projectsMap[projects[i]] = true
   106  	}
   107  	items := make([]argoappv1.Application, 0)
   108  	for i := 0; i < len(apps); i++ {
   109  		a := apps[i]
   110  		if _, ok := projectsMap[a.Spec.GetProject()]; ok {
   111  			items = append(items, a)
   112  		}
   113  	}
   114  	return items
   115  }
   116  
   117  // FilterByProjectsP returns application pointers which belongs to the specified project
   118  func FilterByProjectsP(apps []*argoappv1.Application, projects []string) []*argoappv1.Application {
   119  	if len(projects) == 0 {
   120  		return apps
   121  	}
   122  	projectsMap := make(map[string]bool)
   123  	for i := range projects {
   124  		projectsMap[projects[i]] = true
   125  	}
   126  	items := make([]*argoappv1.Application, 0)
   127  	for i := 0; i < len(apps); i++ {
   128  		a := apps[i]
   129  		if _, ok := projectsMap[a.Spec.GetProject()]; ok {
   130  			items = append(items, a)
   131  		}
   132  	}
   133  	return items
   134  }
   135  
   136  // FilterAppSetsByProjects returns applications which belongs to the specified project
   137  func FilterAppSetsByProjects(appsets []argoappv1.ApplicationSet, projects []string) []argoappv1.ApplicationSet {
   138  	if len(projects) == 0 {
   139  		return appsets
   140  	}
   141  	projectsMap := make(map[string]bool)
   142  	for i := range projects {
   143  		projectsMap[projects[i]] = true
   144  	}
   145  	items := make([]argoappv1.ApplicationSet, 0)
   146  	for i := 0; i < len(appsets); i++ {
   147  		a := appsets[i]
   148  		if _, ok := projectsMap[a.Spec.Template.Spec.GetProject()]; ok {
   149  			items = append(items, a)
   150  		}
   151  	}
   152  	return items
   153  }
   154  
   155  // FilterByRepo returns an application
   156  func FilterByRepo(apps []argoappv1.Application, repo string) []argoappv1.Application {
   157  	if repo == "" {
   158  		return apps
   159  	}
   160  	items := make([]argoappv1.Application, 0)
   161  	for i := 0; i < len(apps); i++ {
   162  		if apps[i].Spec.GetSource().RepoURL == repo {
   163  			items = append(items, apps[i])
   164  		}
   165  	}
   166  	return items
   167  }
   168  
   169  // FilterByRepoP returns application pointers
   170  func FilterByRepoP(apps []*argoappv1.Application, repo string) []*argoappv1.Application {
   171  	if repo == "" {
   172  		return apps
   173  	}
   174  	items := make([]*argoappv1.Application, 0)
   175  	for i := 0; i < len(apps); i++ {
   176  		if apps[i].Spec.GetSource().RepoURL == repo {
   177  			items = append(items, apps[i])
   178  		}
   179  	}
   180  	return items
   181  }
   182  
   183  // FilterByCluster returns an application
   184  func FilterByCluster(apps []argoappv1.Application, cluster string) []argoappv1.Application {
   185  	if cluster == "" {
   186  		return apps
   187  	}
   188  	items := make([]argoappv1.Application, 0)
   189  	for i := 0; i < len(apps); i++ {
   190  		if apps[i].Spec.Destination.Server == cluster || apps[i].Spec.Destination.Name == cluster {
   191  			items = append(items, apps[i])
   192  		}
   193  	}
   194  	return items
   195  }
   196  
   197  // FilterByName returns an application
   198  func FilterByName(apps []argoappv1.Application, name string) ([]argoappv1.Application, error) {
   199  	if name == "" {
   200  		return apps, nil
   201  	}
   202  	items := make([]argoappv1.Application, 0)
   203  	for i := 0; i < len(apps); i++ {
   204  		if apps[i].Name == name {
   205  			items = append(items, apps[i])
   206  			return items, nil
   207  		}
   208  	}
   209  	return items, status.Errorf(codes.NotFound, "application '%s' not found", name)
   210  }
   211  
   212  // FilterByNameP returns pointer applications
   213  // This function is for the changes in #12985.
   214  func FilterByNameP(apps []*argoappv1.Application, name string) []*argoappv1.Application {
   215  	if name == "" {
   216  		return apps
   217  	}
   218  	items := make([]*argoappv1.Application, 0)
   219  	for i := 0; i < len(apps); i++ {
   220  		if apps[i].Name == name {
   221  			items = append(items, apps[i])
   222  			return items
   223  		}
   224  	}
   225  	return items
   226  }
   227  
   228  // RefreshApp updates the refresh annotation of an application to coerce the controller to process it
   229  func RefreshApp(appIf v1alpha1.ApplicationInterface, name string, refreshType argoappv1.RefreshType, hydrate bool) (*argoappv1.Application, error) {
   230  	metadata := map[string]any{
   231  		"metadata": map[string]any{
   232  			"annotations": map[string]string{
   233  				argoappv1.AnnotationKeyRefresh: string(refreshType),
   234  			},
   235  		},
   236  	}
   237  	if hydrate {
   238  		metadata["metadata"].(map[string]any)["annotations"].(map[string]string)[argoappv1.AnnotationKeyHydrate] = string(argoappv1.HydrateTypeNormal)
   239  	}
   240  
   241  	var err error
   242  	patch, err := json.Marshal(metadata)
   243  	if err != nil {
   244  		return nil, fmt.Errorf("error marshaling metadata: %w", err)
   245  	}
   246  	for attempt := 0; attempt < 5; attempt++ {
   247  		app, err := appIf.Patch(context.Background(), name, types.MergePatchType, patch, metav1.PatchOptions{})
   248  		if err == nil {
   249  			log.Infof("Requested app '%s' refresh", name)
   250  			return app.DeepCopy(), nil
   251  		}
   252  		if !apierrors.IsConflict(err) {
   253  			return nil, fmt.Errorf("error patching annotations in application %q: %w", name, err)
   254  		}
   255  		time.Sleep(100 * time.Millisecond)
   256  	}
   257  	return nil, err
   258  }
   259  
   260  func TestRepoWithKnownType(ctx context.Context, repoClient apiclient.RepoServerServiceClient, repo *argoappv1.Repository, isHelm bool, isHelmOci bool, isOCI bool) error {
   261  	repo = repo.DeepCopy()
   262  	switch {
   263  	case isHelm:
   264  		repo.Type = "helm"
   265  	case isOCI:
   266  		repo.Type = "oci"
   267  	case repo.Type != "oci":
   268  		repo.Type = "git"
   269  	}
   270  	repo.EnableOCI = repo.EnableOCI || isHelmOci
   271  
   272  	_, err := repoClient.TestRepository(ctx, &apiclient.TestRepositoryRequest{
   273  		Repo: repo,
   274  	})
   275  	if err != nil {
   276  		return fmt.Errorf("repo client error while testing repository: %w", err)
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  // ValidateRepo validates the repository specified in application spec. Following is checked:
   283  // * the repository is accessible
   284  // * the path contains valid manifests
   285  // * there are parameters of only one app source type
   286  //
   287  // The plugins parameter is no longer used. It is kept for compatibility with the old signature until Argo CD v3.0.
   288  func ValidateRepo(
   289  	ctx context.Context,
   290  	app *argoappv1.Application,
   291  	repoClientset apiclient.Clientset,
   292  	db db.ArgoDB,
   293  	kubectl kube.Kubectl,
   294  	proj *argoappv1.AppProject,
   295  	settingsMgr *settings.SettingsManager,
   296  ) ([]argoappv1.ApplicationCondition, error) {
   297  	spec := &app.Spec
   298  
   299  	conditions := make([]argoappv1.ApplicationCondition, 0)
   300  
   301  	// Test the repo
   302  	conn, repoClient, err := repoClientset.NewRepoServerClient()
   303  	if err != nil {
   304  		return nil, fmt.Errorf("error instantiating new repo server client: %w", err)
   305  	}
   306  	defer utilio.Close(conn)
   307  
   308  	helmOptions, err := settingsMgr.GetHelmSettings()
   309  	if err != nil {
   310  		return nil, fmt.Errorf("error getting helm settings: %w", err)
   311  	}
   312  
   313  	helmRepos, err := db.ListHelmRepositories(ctx)
   314  	if err != nil {
   315  		return nil, fmt.Errorf("error listing helm repos: %w", err)
   316  	}
   317  	permittedHelmRepos, err := GetPermittedRepos(proj, helmRepos)
   318  	if err != nil {
   319  		return nil, fmt.Errorf("error getting permitted repos: %w", err)
   320  	}
   321  	helmRepositoryCredentials, err := db.GetAllHelmRepositoryCredentials(ctx)
   322  	if err != nil {
   323  		return nil, fmt.Errorf("error getting helm repo creds: %w", err)
   324  	}
   325  	ociRepos, err := db.ListOCIRepositories(ctx)
   326  	if err != nil {
   327  		return nil, fmt.Errorf("failed to list oci repositories: %w", err)
   328  	}
   329  	permittedOCIRepos, err := GetPermittedRepos(proj, ociRepos)
   330  	if err != nil {
   331  		return nil, fmt.Errorf("failed to get permitted oci repositories for project %q: %w", proj.Name, err)
   332  	}
   333  	permittedHelmCredentials, err := GetPermittedReposCredentials(proj, helmRepositoryCredentials)
   334  	if err != nil {
   335  		return nil, fmt.Errorf("error getting permitted repo creds: %w", err)
   336  	}
   337  	ociRepositoryCredentials, err := db.GetAllOCIRepositoryCredentials(ctx)
   338  	if err != nil {
   339  		return nil, fmt.Errorf("failed to get OCI credentials: %w", err)
   340  	}
   341  	permittedOCICredentials, err := GetPermittedReposCredentials(proj, ociRepositoryCredentials)
   342  	if err != nil {
   343  		return nil, fmt.Errorf("failed to get permitted OCI credentials for project %q: %w", proj.Name, err)
   344  	}
   345  
   346  	destCluster, err := GetDestinationCluster(ctx, spec.Destination, db)
   347  	if err != nil {
   348  		conditions = append(conditions, argoappv1.ApplicationCondition{
   349  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   350  			Message: fmt.Sprintf("Unable to get cluster: %v", err),
   351  		})
   352  		return conditions, nil
   353  	}
   354  	config, err := destCluster.RESTConfig()
   355  	if err != nil {
   356  		return nil, fmt.Errorf("error getting cluster REST config: %w", err)
   357  	}
   358  	//nolint:staticcheck
   359  	destCluster.ServerVersion, err = kubectl.GetServerVersion(config)
   360  	if err != nil {
   361  		return nil, fmt.Errorf("error getting k8s server version: %w", err)
   362  	}
   363  	apiGroups, err := kubectl.GetAPIResources(config, false, cache.NewNoopSettings())
   364  	if err != nil {
   365  		return nil, fmt.Errorf("error getting API resources: %w", err)
   366  	}
   367  	enabledSourceTypes, err := settingsMgr.GetEnabledSourceTypes()
   368  	if err != nil {
   369  		return nil, fmt.Errorf("error getting enabled source types: %w", err)
   370  	}
   371  
   372  	sourceCondition, err := validateRepo(
   373  		ctx,
   374  		app,
   375  		db,
   376  		app.Spec.GetSources(),
   377  		repoClient,
   378  		permittedHelmRepos,
   379  		permittedOCIRepos,
   380  		helmOptions,
   381  		destCluster,
   382  		apiGroups,
   383  		proj,
   384  		permittedHelmCredentials,
   385  		permittedOCICredentials,
   386  		enabledSourceTypes,
   387  		settingsMgr)
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  	conditions = append(conditions, sourceCondition...)
   392  
   393  	return conditions, nil
   394  }
   395  
   396  func validateRepo(ctx context.Context,
   397  	app *argoappv1.Application,
   398  	db db.ArgoDB,
   399  	sources []argoappv1.ApplicationSource,
   400  	repoClient apiclient.RepoServerServiceClient,
   401  	permittedHelmRepos []*argoappv1.Repository,
   402  	permittedOCIRepos []*argoappv1.Repository,
   403  	helmOptions *argoappv1.HelmOptions,
   404  	cluster *argoappv1.Cluster,
   405  	apiGroups []kube.APIResourceInfo,
   406  	proj *argoappv1.AppProject,
   407  	permittedHelmCredentials []*argoappv1.RepoCreds,
   408  	permittedOCICredentials []*argoappv1.RepoCreds,
   409  	enabledSourceTypes map[string]bool,
   410  	settingsMgr *settings.SettingsManager,
   411  ) ([]argoappv1.ApplicationCondition, error) {
   412  	conditions := make([]argoappv1.ApplicationCondition, 0)
   413  	errMessage := ""
   414  
   415  	for _, source := range sources {
   416  		repo, err := db.GetRepository(ctx, source.RepoURL, proj.Name)
   417  		if err != nil {
   418  			return nil, err
   419  		}
   420  		if err := TestRepoWithKnownType(ctx, repoClient, repo, source.IsHelm(), source.IsHelmOci(), source.IsOCI()); err != nil {
   421  			errMessage = fmt.Sprintf("repositories not accessible: %v: %v", repo.StringForLogging(), err)
   422  		}
   423  		repoAccessible := false
   424  
   425  		if errMessage != "" {
   426  			conditions = append(conditions, argoappv1.ApplicationCondition{
   427  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   428  				Message: fmt.Sprintf("repository not accessible: %v", errMessage),
   429  			})
   430  		} else {
   431  			repoAccessible = true
   432  		}
   433  
   434  		// Verify only one source type is defined
   435  		_, err = source.ExplicitType()
   436  		if err != nil {
   437  			return nil, fmt.Errorf("error verifying source type: %w", err)
   438  		}
   439  
   440  		// is the repo inaccessible - abort now
   441  		if !repoAccessible {
   442  			return conditions, nil
   443  		}
   444  	}
   445  
   446  	// If using the source hydrator, check the dry source instead of the sync source, since the sync source branch may
   447  	// not exist yet.
   448  	if app.Spec.SourceHydrator != nil {
   449  		sources = []argoappv1.ApplicationSource{app.Spec.SourceHydrator.GetDrySource()}
   450  	}
   451  
   452  	refSources, err := GetRefSources(ctx, sources, app.Spec.Project, db.GetRepository, []string{})
   453  	if err != nil {
   454  		return nil, fmt.Errorf("error getting ref sources: %w", err)
   455  	}
   456  	conditions = append(conditions, verifyGenerateManifests(
   457  		ctx,
   458  		db,
   459  		permittedHelmRepos,
   460  		permittedOCIRepos,
   461  		helmOptions,
   462  		app,
   463  		proj,
   464  		sources,
   465  		repoClient,
   466  		//nolint:staticcheck
   467  		cluster.ServerVersion,
   468  		APIResourcesToStrings(apiGroups, true),
   469  		permittedHelmCredentials,
   470  		permittedOCICredentials,
   471  		enabledSourceTypes,
   472  		settingsMgr,
   473  		refSources)...)
   474  
   475  	return conditions, nil
   476  }
   477  
   478  // GetRefSources creates a map of ref keys (from the sources' 'ref' fields) to information about the referenced source.
   479  // This function also validates the references use allowed characters and does not define the same ref key more than
   480  // once (which would lead to ambiguous references).
   481  func GetRefSources(ctx context.Context, sources argoappv1.ApplicationSources, project string, getRepository func(ctx context.Context, url string, project string) (*argoappv1.Repository, error), revisions []string) (argoappv1.RefTargetRevisionMapping, error) {
   482  	refSources := make(argoappv1.RefTargetRevisionMapping)
   483  	if len(sources) > 1 {
   484  		// Validate first to avoid unnecessary DB calls.
   485  		refKeys := make(map[string]bool)
   486  		for _, source := range sources {
   487  			if source.Ref == "" {
   488  				continue
   489  			}
   490  			isValidRefKey := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString
   491  			if !isValidRefKey(source.Ref) {
   492  				return nil, fmt.Errorf("sources.ref %s cannot contain any special characters except '_' and '-'", source.Ref)
   493  			}
   494  			refKey := "$" + source.Ref
   495  			if _, ok := refKeys[refKey]; ok {
   496  				return nil, errors.New("invalid sources: multiple sources had the same `ref` key")
   497  			}
   498  			refKeys[refKey] = true
   499  		}
   500  		// Get Repositories for all sources before generating Manifests
   501  		for i, source := range sources {
   502  			if source.Ref == "" {
   503  				continue
   504  			}
   505  
   506  			repo, err := getRepository(ctx, source.RepoURL, project)
   507  			if err != nil {
   508  				return nil, fmt.Errorf("failed to get repository %s: %w", source.RepoURL, err)
   509  			}
   510  			refKey := "$" + source.Ref
   511  			revision := source.TargetRevision
   512  			if len(revisions) > i && revisions[i] != "" {
   513  				revision = revisions[i]
   514  			}
   515  			refSources[refKey] = &argoappv1.RefTarget{
   516  				Repo:           *repo,
   517  				TargetRevision: revision,
   518  				Chart:          source.Chart,
   519  			}
   520  		}
   521  	}
   522  	return refSources, nil
   523  }
   524  
   525  func validateSourcePermissions(source argoappv1.ApplicationSource, hasMultipleSources bool) []argoappv1.ApplicationCondition {
   526  	var conditions []argoappv1.ApplicationCondition
   527  	if hasMultipleSources {
   528  		if source.RepoURL == "" || (source.Path == "" && source.Chart == "" && source.Ref == "") {
   529  			conditions = append(conditions, argoappv1.ApplicationCondition{
   530  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   531  				Message: fmt.Sprintf("spec.source.repoURL and either source.path, source.chart, or source.ref are required for source %s", source),
   532  			})
   533  			return conditions
   534  		}
   535  	} else {
   536  		if source.RepoURL == "" || (source.Path == "" && source.Chart == "") {
   537  			conditions = append(conditions, argoappv1.ApplicationCondition{
   538  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   539  				Message: "spec.source.repoURL and either spec.source.path or spec.source.chart are required",
   540  			})
   541  			return conditions
   542  		}
   543  	}
   544  	if source.Chart != "" && source.TargetRevision == "" {
   545  		conditions = append(conditions, argoappv1.ApplicationCondition{
   546  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   547  			Message: "spec.source.targetRevision is required if the manifest source is a helm chart",
   548  		})
   549  		return conditions
   550  	}
   551  
   552  	return conditions
   553  }
   554  
   555  func validateSourceHydrator(hydrator *argoappv1.SourceHydrator) []argoappv1.ApplicationCondition {
   556  	var conditions []argoappv1.ApplicationCondition
   557  	if hydrator.DrySource.RepoURL == "" {
   558  		conditions = append(conditions, argoappv1.ApplicationCondition{
   559  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   560  			Message: "spec.sourceHydrator.drySource.repoURL is required",
   561  		})
   562  	}
   563  	if hydrator.SyncSource.TargetBranch == "" {
   564  		conditions = append(conditions, argoappv1.ApplicationCondition{
   565  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   566  			Message: "spec.sourceHydrator.syncSource.targetBranch is required",
   567  		})
   568  	}
   569  	if hydrator.HydrateTo != nil && hydrator.HydrateTo.TargetBranch == "" {
   570  		conditions = append(conditions, argoappv1.ApplicationCondition{
   571  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   572  			Message: "when spec.sourceHydrator.hydrateTo is set, spec.sourceHydrator.hydrateTo.path is required",
   573  		})
   574  	}
   575  	return conditions
   576  }
   577  
   578  // 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
   579  func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, proj *argoappv1.AppProject, db db.ArgoDB) ([]argoappv1.ApplicationCondition, error) {
   580  	conditions := make([]argoappv1.ApplicationCondition, 0)
   581  
   582  	switch {
   583  	case spec.SourceHydrator != nil:
   584  		condition := validateSourceHydrator(spec.SourceHydrator)
   585  		if len(condition) > 0 {
   586  			conditions = append(conditions, condition...)
   587  			return conditions, nil
   588  		}
   589  		if !proj.IsSourcePermitted(spec.SourceHydrator.GetDrySource()) {
   590  			conditions = append(conditions, argoappv1.ApplicationCondition{
   591  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   592  				Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.SourceHydrator.GetDrySource().RepoURL, proj.Name),
   593  			})
   594  		}
   595  	case spec.HasMultipleSources():
   596  		for _, source := range spec.Sources {
   597  			condition := validateSourcePermissions(source, spec.HasMultipleSources())
   598  			if len(condition) > 0 {
   599  				conditions = append(conditions, condition...)
   600  				return conditions, nil
   601  			}
   602  
   603  			if !proj.IsSourcePermitted(source) {
   604  				conditions = append(conditions, argoappv1.ApplicationCondition{
   605  					Type:    argoappv1.ApplicationConditionInvalidSpecError,
   606  					Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", source.RepoURL, proj.Name),
   607  				})
   608  			}
   609  		}
   610  	default:
   611  		conditions = validateSourcePermissions(spec.GetSource(), spec.HasMultipleSources())
   612  		if len(conditions) > 0 {
   613  			return conditions, nil
   614  		}
   615  
   616  		if !proj.IsSourcePermitted(spec.GetSource()) {
   617  			conditions = append(conditions, argoappv1.ApplicationCondition{
   618  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   619  				Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.GetSource().RepoURL, proj.Name),
   620  			})
   621  		}
   622  	}
   623  
   624  	destCluster, err := GetDestinationCluster(ctx, spec.Destination, db)
   625  	if err != nil {
   626  		conditions = append(conditions, argoappv1.ApplicationCondition{
   627  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   628  			Message: err.Error(),
   629  		})
   630  		return conditions, nil
   631  	}
   632  	permitted, err := proj.IsDestinationPermitted(destCluster, spec.Destination.Namespace, func(project string) ([]*argoappv1.Cluster, error) {
   633  		return db.GetProjectClusters(ctx, project)
   634  	})
   635  	if err != nil {
   636  		return nil, err
   637  	}
   638  	if !permitted {
   639  		server := destCluster.Server
   640  		if spec.Destination.Name != "" {
   641  			server = destCluster.Name
   642  		}
   643  		conditions = append(conditions, argoappv1.ApplicationCondition{
   644  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   645  			Message: fmt.Sprintf("application destination server '%s' and namespace '%s' do not match any of the allowed destinations in project '%s'", server, spec.Destination.Namespace, proj.Name),
   646  		})
   647  	}
   648  	return conditions, nil
   649  }
   650  
   651  // APIResourcesToStrings converts list of API Resources list into string list
   652  func APIResourcesToStrings(resources []kube.APIResourceInfo, includeKinds bool) []string {
   653  	resMap := map[string]bool{}
   654  	for _, r := range resources {
   655  		groupVersion := r.GroupVersionResource.GroupVersion().String()
   656  		resMap[groupVersion] = true
   657  		if includeKinds {
   658  			resMap[groupVersion+"/"+r.GroupKind.Kind] = true
   659  		}
   660  	}
   661  	var res []string
   662  	for k := range resMap {
   663  		res = append(res, k)
   664  	}
   665  	sort.Slice(res, func(i, j int) bool {
   666  		return res[i] < res[j]
   667  	})
   668  	return res
   669  }
   670  
   671  // GetAppProjectWithScopedResources returns a project from an application with scoped resources
   672  func GetAppProjectWithScopedResources(ctx context.Context, name string, projLister applicationsv1.AppProjectLister, ns string, settingsManager *settings.SettingsManager, db db.ArgoDB) (*argoappv1.AppProject, argoappv1.Repositories, []*argoappv1.Cluster, error) {
   673  	projOrig, err := projLister.AppProjects(ns).Get(name)
   674  	if err != nil {
   675  		return nil, nil, nil, fmt.Errorf("error getting app project %q: %w", name, err)
   676  	}
   677  
   678  	project, err := GetAppVirtualProject(projOrig, projLister, settingsManager)
   679  	if err != nil {
   680  		return nil, nil, nil, fmt.Errorf("error getting app virtual project: %w", err)
   681  	}
   682  
   683  	clusters, err := db.GetProjectClusters(ctx, project.Name)
   684  	if err != nil {
   685  		return nil, nil, nil, fmt.Errorf("error getting project clusters: %w", err)
   686  	}
   687  	repos, err := db.GetProjectRepositories(name)
   688  	if err != nil {
   689  		return nil, nil, nil, fmt.Errorf("error getting project repos: %w", err)
   690  	}
   691  	return project, repos, clusters, nil
   692  }
   693  
   694  // GetAppProjectByName returns a project from an application based on name
   695  func GetAppProjectByName(ctx context.Context, name string, projLister applicationsv1.AppProjectLister, ns string, settingsManager *settings.SettingsManager, db db.ArgoDB) (*argoappv1.AppProject, error) {
   696  	projOrig, err := projLister.AppProjects(ns).Get(name)
   697  	if err != nil {
   698  		return nil, fmt.Errorf("error getting app project %q: %w", name, err)
   699  	}
   700  	project := projOrig.DeepCopy()
   701  	repos, err := db.GetProjectRepositories(name)
   702  	if err != nil {
   703  		return nil, fmt.Errorf("error getting project repositories: %w", err)
   704  	}
   705  	for _, repo := range repos {
   706  		project.Spec.SourceRepos = append(project.Spec.SourceRepos, repo.Repo)
   707  	}
   708  	clusters, err := db.GetProjectClusters(ctx, name)
   709  	if err != nil {
   710  		return nil, fmt.Errorf("error getting project clusters: %w", err)
   711  	}
   712  	for _, cluster := range clusters {
   713  		if len(cluster.Namespaces) == 0 {
   714  			project.Spec.Destinations = append(project.Spec.Destinations, argoappv1.ApplicationDestination{Server: cluster.Server, Namespace: "*"})
   715  		} else {
   716  			for _, ns := range cluster.Namespaces {
   717  				project.Spec.Destinations = append(project.Spec.Destinations, argoappv1.ApplicationDestination{Server: cluster.Server, Namespace: ns})
   718  			}
   719  		}
   720  	}
   721  	return GetAppVirtualProject(project, projLister, settingsManager)
   722  }
   723  
   724  // GetAppProject returns a project from an application. It will also ensure
   725  // that the application is allowed to use the project.
   726  func GetAppProject(ctx context.Context, app *argoappv1.Application, projLister applicationsv1.AppProjectLister, ns string, settingsManager *settings.SettingsManager, db db.ArgoDB) (*argoappv1.AppProject, error) {
   727  	proj, err := GetAppProjectByName(ctx, app.Spec.GetProject(), projLister, ns, settingsManager, db)
   728  	if err != nil {
   729  		return nil, err
   730  	}
   731  	if !proj.IsAppNamespacePermitted(app, ns) {
   732  		return nil, NewErrApplicationNotAllowedToUseProject(app.Name, app.Namespace, proj.Name)
   733  	}
   734  	return proj, nil
   735  }
   736  
   737  // verifyGenerateManifests verifies a repo path can generate manifests
   738  func verifyGenerateManifests(
   739  	ctx context.Context,
   740  	db db.ArgoDB,
   741  	helmRepos argoappv1.Repositories,
   742  	ociRepos argoappv1.Repositories,
   743  	helmOptions *argoappv1.HelmOptions,
   744  	app *argoappv1.Application,
   745  	proj *argoappv1.AppProject,
   746  	sources []argoappv1.ApplicationSource,
   747  	repoClient apiclient.RepoServerServiceClient,
   748  	kubeVersion string,
   749  	apiVersions []string,
   750  	repositoryCredentials []*argoappv1.RepoCreds,
   751  	ociRepositoryCredentials []*argoappv1.RepoCreds,
   752  	enableGenerateManifests map[string]bool,
   753  	settingsMgr *settings.SettingsManager,
   754  	refSources argoappv1.RefTargetRevisionMapping,
   755  ) []argoappv1.ApplicationCondition {
   756  	var conditions []argoappv1.ApplicationCondition
   757  	// If source is Kustomize add build options
   758  	kustomizeSettings, err := settingsMgr.GetKustomizeSettings()
   759  	if err != nil {
   760  		conditions = append(conditions, argoappv1.ApplicationCondition{
   761  			Type:    argoappv1.ApplicationConditionInvalidSpecError,
   762  			Message: fmt.Sprintf("Error getting Kustomize settings: %v", err),
   763  		})
   764  		return conditions // Can't perform the next check without settings.
   765  	}
   766  
   767  	for _, source := range sources {
   768  		repoRes, err := db.GetRepository(ctx, source.RepoURL, proj.Name)
   769  		if err != nil {
   770  			conditions = append(conditions, argoappv1.ApplicationCondition{
   771  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   772  				Message: fmt.Sprintf("Unable to get repository: %v", err),
   773  			})
   774  			continue
   775  		}
   776  		installationID, err := settingsMgr.GetInstallationID()
   777  		if err != nil {
   778  			conditions = append(conditions, argoappv1.ApplicationCondition{
   779  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   780  				Message: fmt.Sprintf("Error getting installation ID: %v", err),
   781  			})
   782  			continue
   783  		}
   784  
   785  		appLabelKey, err := settingsMgr.GetAppInstanceLabelKey()
   786  		if err != nil {
   787  			conditions = append(conditions, argoappv1.ApplicationCondition{
   788  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   789  				Message: fmt.Sprintf("Error getting app label key ID: %v", err),
   790  			})
   791  			continue
   792  		}
   793  
   794  		trackingMethod, err := settingsMgr.GetTrackingMethod()
   795  		if err != nil {
   796  			conditions = append(conditions, argoappv1.ApplicationCondition{
   797  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   798  				Message: fmt.Sprintf("Error getting trackingMethod: %v", err),
   799  			})
   800  			continue
   801  		}
   802  
   803  		verifySignature := false
   804  		if len(proj.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
   805  			verifySignature = true
   806  		}
   807  
   808  		repos := helmRepos
   809  		helmRepoCreds := repositoryCredentials
   810  		// If the source is OCI, there is a potential for an OCI image to be a Helm chart and that said chart in
   811  		// turn would have OCI dependencies. To ensure that those dependencies can be resolved, add them to the repos
   812  		// list.
   813  		if source.IsOCI() {
   814  			repos = slices.Clone(helmRepos)
   815  			helmRepoCreds = slices.Clone(repositoryCredentials)
   816  			repos = append(repos, ociRepos...)
   817  			helmRepoCreds = append(helmRepoCreds, ociRepositoryCredentials...)
   818  		}
   819  
   820  		req := apiclient.ManifestRequest{
   821  			Repo: &argoappv1.Repository{
   822  				Repo:    source.RepoURL,
   823  				Type:    repoRes.Type,
   824  				Name:    repoRes.Name,
   825  				Proxy:   repoRes.Proxy,
   826  				NoProxy: repoRes.NoProxy,
   827  			},
   828  			VerifySignature:                 verifySignature,
   829  			Repos:                           repos,
   830  			Revision:                        source.TargetRevision,
   831  			AppName:                         app.Name,
   832  			Namespace:                       app.Spec.Destination.Namespace,
   833  			ApplicationSource:               &source,
   834  			AppLabelKey:                     appLabelKey,
   835  			KustomizeOptions:                kustomizeSettings,
   836  			KubeVersion:                     kubeVersion,
   837  			ApiVersions:                     apiVersions,
   838  			HelmOptions:                     helmOptions,
   839  			HelmRepoCreds:                   helmRepoCreds,
   840  			TrackingMethod:                  trackingMethod,
   841  			EnabledSourceTypes:              enableGenerateManifests,
   842  			NoRevisionCache:                 true,
   843  			HasMultipleSources:              app.Spec.HasMultipleSources(),
   844  			RefSources:                      refSources,
   845  			ProjectName:                     proj.Name,
   846  			ProjectSourceRepos:              proj.Spec.SourceRepos,
   847  			AnnotationManifestGeneratePaths: app.GetAnnotation(argoappv1.AnnotationKeyManifestGeneratePaths),
   848  			InstallationID:                  installationID,
   849  		}
   850  		req.Repo.CopyCredentialsFromRepo(repoRes)
   851  		req.Repo.CopySettingsFrom(repoRes)
   852  
   853  		// Only check whether we can access the application's path,
   854  		// and not whether it actually contains any manifests.
   855  		_, err = repoClient.GenerateManifest(ctx, &req)
   856  		if err != nil {
   857  			errMessage := fmt.Sprintf("Unable to generate manifests in %s: %s", source.Path, err)
   858  			conditions = append(conditions, argoappv1.ApplicationCondition{
   859  				Type:    argoappv1.ApplicationConditionInvalidSpecError,
   860  				Message: errMessage,
   861  			})
   862  		}
   863  	}
   864  
   865  	return conditions
   866  }
   867  
   868  // SetAppOperation updates an application with the specified operation, retrying conflict errors
   869  func SetAppOperation(appIf v1alpha1.ApplicationInterface, appName string, op *argoappv1.Operation) (*argoappv1.Application, error) {
   870  	for {
   871  		a, err := appIf.Get(context.Background(), appName, metav1.GetOptions{})
   872  		if err != nil {
   873  			return nil, fmt.Errorf("error getting application %q: %w", appName, err)
   874  		}
   875  		a = a.DeepCopy()
   876  		if a.Operation != nil {
   877  			return nil, ErrAnotherOperationInProgress
   878  		}
   879  		a.Operation = op
   880  		a.Status.OperationState = nil
   881  		a, err = appIf.Update(context.Background(), a, metav1.UpdateOptions{})
   882  		if op.Sync == nil {
   883  			return nil, status.Errorf(codes.InvalidArgument, "Operation unspecified")
   884  		}
   885  		if err == nil {
   886  			return a, nil
   887  		}
   888  		if !apierrors.IsConflict(err) {
   889  			return nil, fmt.Errorf("error updating application %q: %w", appName, err)
   890  		}
   891  		log.Warnf("Failed to set operation for app '%s' due to update conflict. Retrying again...", appName)
   892  	}
   893  }
   894  
   895  // ContainsSyncResource determines if the given resource exists in the provided slice of sync operation resources.
   896  func ContainsSyncResource(name string, namespace string, gvk schema.GroupVersionKind, rr []argoappv1.SyncOperationResource) bool {
   897  	for _, r := range rr {
   898  		if r.HasIdentity(name, namespace, gvk) {
   899  			return true
   900  		}
   901  	}
   902  	return false
   903  }
   904  
   905  // IncludeResource The app resource is checked against the include or exclude filters.
   906  // If exclude filters are present, they are evaluated only after all include filters have been assessed.
   907  func IncludeResource(resourceName string, resourceNamespace string, gvk schema.GroupVersionKind,
   908  	syncOperationResources []*argoappv1.SyncOperationResource,
   909  ) bool {
   910  	includeResource := false
   911  	foundIncludeRule := false
   912  	// Evaluate include filters only in this loop.
   913  	for _, syncOperationResource := range syncOperationResources {
   914  		if syncOperationResource.Exclude {
   915  			continue
   916  		}
   917  		foundIncludeRule = true
   918  		includeResource = syncOperationResource.Compare(resourceName, resourceNamespace, gvk)
   919  		if includeResource {
   920  			break
   921  		}
   922  	}
   923  
   924  	// if a resource is present both in include and in exclude, the exclude wins.
   925  	// that including it here is a temporary decision for the use case when no include rules exist,
   926  	// but it still might be excluded later if it matches an exclude rule:
   927  	if !foundIncludeRule {
   928  		includeResource = true
   929  	}
   930  	// No needs to evaluate exclude filters when the resource is not included.
   931  	if !includeResource {
   932  		return false
   933  	}
   934  	// Evaluate exclude filters only in this loop.
   935  	for _, syncOperationResource := range syncOperationResources {
   936  		if syncOperationResource.Exclude && syncOperationResource.Compare(resourceName, resourceNamespace, gvk) {
   937  			return false
   938  		}
   939  	}
   940  	return true
   941  }
   942  
   943  // NormalizeApplicationSpec will normalize an application spec to a preferred state. This is used
   944  // for migrating application objects which are using deprecated legacy fields into the new fields,
   945  // and defaulting fields in the spec (e.g. spec.project)
   946  func NormalizeApplicationSpec(spec *argoappv1.ApplicationSpec) *argoappv1.ApplicationSpec {
   947  	spec = spec.DeepCopy()
   948  	if spec.Project == "" {
   949  		spec.Project = argoappv1.DefaultAppProjectName
   950  	}
   951  	if spec.SyncPolicy.IsZero() {
   952  		spec.SyncPolicy = nil
   953  	}
   954  	if len(spec.Sources) > 0 {
   955  		for _, source := range spec.Sources {
   956  			NormalizeSource(&source)
   957  		}
   958  	} else if spec.Source != nil {
   959  		// In practice, spec.Source should never be nil.
   960  		NormalizeSource(spec.Source)
   961  	}
   962  	return spec
   963  }
   964  
   965  func NormalizeSource(source *argoappv1.ApplicationSource) *argoappv1.ApplicationSource {
   966  	// 3. If any app sources are their zero values, then nil out the pointers to the source spec.
   967  	// This makes it easier for users to switch between app source types if they are not using
   968  	// any of the source-specific parameters.
   969  	if source.Kustomize != nil && source.Kustomize.IsZero() {
   970  		source.Kustomize = nil
   971  	}
   972  	if source.Helm != nil && source.Helm.IsZero() {
   973  		source.Helm = nil
   974  	}
   975  	if source.Directory != nil && source.Directory.IsZero() {
   976  		switch {
   977  		case source.Directory.Exclude != "" && source.Directory.Include != "":
   978  			source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: source.Directory.Exclude, Include: source.Directory.Include}
   979  		case source.Directory.Exclude != "":
   980  			source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: source.Directory.Exclude}
   981  		case source.Directory.Include != "":
   982  			source.Directory = &argoappv1.ApplicationSourceDirectory{Include: source.Directory.Include}
   983  		default:
   984  			source.Directory = nil
   985  		}
   986  	}
   987  	return source
   988  }
   989  
   990  func GetPermittedReposCredentials(proj *argoappv1.AppProject, repoCreds []*argoappv1.RepoCreds) ([]*argoappv1.RepoCreds, error) {
   991  	var permittedRepoCreds []*argoappv1.RepoCreds
   992  	for _, v := range repoCreds {
   993  		if proj.IsSourcePermitted(argoappv1.ApplicationSource{RepoURL: v.URL}) {
   994  			permittedRepoCreds = append(permittedRepoCreds, v)
   995  		}
   996  	}
   997  	return permittedRepoCreds, nil
   998  }
   999  
  1000  func GetPermittedRepos(proj *argoappv1.AppProject, repos []*argoappv1.Repository) ([]*argoappv1.Repository, error) {
  1001  	var permittedRepos []*argoappv1.Repository
  1002  	for _, v := range repos {
  1003  		if proj.IsSourcePermitted(argoappv1.ApplicationSource{RepoURL: v.Repo}) {
  1004  			permittedRepos = append(permittedRepos, v)
  1005  		}
  1006  	}
  1007  	return permittedRepos, nil
  1008  }
  1009  
  1010  type ClusterGetter interface {
  1011  	GetCluster(ctx context.Context, name string) (*argoappv1.Cluster, error)
  1012  	GetClusterServersByName(ctx context.Context, server string) ([]string, error)
  1013  }
  1014  
  1015  // GetDestinationCluster returns the cluster object based on the destination server or name. If both are provided or
  1016  // both are empty, an error is returned. If the destination server is provided, the cluster is fetched by the server
  1017  // URL. If the destination name is provided, the cluster is fetched by the name. If multiple clusters have the specified
  1018  // name, an error is returned.
  1019  func GetDestinationCluster(ctx context.Context, destination argoappv1.ApplicationDestination, db ClusterGetter) (*argoappv1.Cluster, error) {
  1020  	if destination.Name != "" && destination.Server != "" {
  1021  		return nil, fmt.Errorf("application destination can't have both name and server defined: %s %s", destination.Name, destination.Server)
  1022  	}
  1023  	if destination.Server != "" {
  1024  		cluster, err := db.GetCluster(ctx, destination.Server)
  1025  		if err != nil {
  1026  			return nil, fmt.Errorf("error getting cluster by server %q: %w", destination.Server, err)
  1027  		}
  1028  		return cluster, nil
  1029  	} else if destination.Name != "" {
  1030  		clusterURLs, err := db.GetClusterServersByName(ctx, destination.Name)
  1031  		if err != nil {
  1032  			return nil, fmt.Errorf("error getting cluster by name %q: %w", destination.Name, err)
  1033  		}
  1034  		if len(clusterURLs) == 0 {
  1035  			return nil, fmt.Errorf("there are no clusters with this name: %s", destination.Name)
  1036  		}
  1037  		if len(clusterURLs) > 1 {
  1038  			return nil, fmt.Errorf("there are %d clusters with the same name: [%s]", len(clusterURLs), strings.Join(clusterURLs, " "))
  1039  		}
  1040  		cluster, err := db.GetCluster(ctx, clusterURLs[0])
  1041  		if err != nil {
  1042  			return nil, fmt.Errorf("error getting cluster by URL: %w", err)
  1043  		}
  1044  		return cluster, nil
  1045  	}
  1046  	// nolint:staticcheck // Error constant is very old, shouldn't lowercase the first letter.
  1047  	return nil, errors.New(ErrDestinationMissing)
  1048  }
  1049  
  1050  func GetGlobalProjects(proj *argoappv1.AppProject, projLister applicationsv1.AppProjectLister, settingsManager *settings.SettingsManager) []*argoappv1.AppProject {
  1051  	gps, err := settingsManager.GetGlobalProjectsSettings()
  1052  	globalProjects := make([]*argoappv1.AppProject, 0)
  1053  
  1054  	if err != nil {
  1055  		log.Warnf("Failed to get global project settings: %v", err)
  1056  		return globalProjects
  1057  	}
  1058  
  1059  	for _, gp := range gps {
  1060  		// The project itself is not its own the global project
  1061  		if proj.Name == gp.ProjectName {
  1062  			continue
  1063  		}
  1064  
  1065  		selector, err := metav1.LabelSelectorAsSelector(&gp.LabelSelector)
  1066  		if err != nil {
  1067  			break
  1068  		}
  1069  		// Get projects which match the label selector, then see if proj is a match
  1070  		projList, err := projLister.AppProjects(proj.Namespace).List(selector)
  1071  		if err != nil {
  1072  			break
  1073  		}
  1074  		var matchMe bool
  1075  		for _, item := range projList {
  1076  			if item.Name == proj.Name {
  1077  				matchMe = true
  1078  				break
  1079  			}
  1080  		}
  1081  		if !matchMe {
  1082  			continue
  1083  		}
  1084  		// If proj is a match for this global project setting, then it is its global project
  1085  		globalProj, err := projLister.AppProjects(proj.Namespace).Get(gp.ProjectName)
  1086  		if err != nil {
  1087  			break
  1088  		}
  1089  		globalProjects = append(globalProjects, globalProj)
  1090  	}
  1091  	return globalProjects
  1092  }
  1093  
  1094  func GetAppVirtualProject(proj *argoappv1.AppProject, projLister applicationsv1.AppProjectLister, settingsManager *settings.SettingsManager) (*argoappv1.AppProject, error) {
  1095  	virtualProj := proj.DeepCopy()
  1096  	globalProjects := GetGlobalProjects(proj, projLister, settingsManager)
  1097  
  1098  	for _, gp := range globalProjects {
  1099  		virtualProj = mergeVirtualProject(virtualProj, gp)
  1100  	}
  1101  	return virtualProj, nil
  1102  }
  1103  
  1104  func mergeVirtualProject(proj *argoappv1.AppProject, globalProj *argoappv1.AppProject) *argoappv1.AppProject {
  1105  	if globalProj == nil {
  1106  		return proj
  1107  	}
  1108  	proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, globalProj.Spec.ClusterResourceWhitelist...)
  1109  	proj.Spec.ClusterResourceBlacklist = append(proj.Spec.ClusterResourceBlacklist, globalProj.Spec.ClusterResourceBlacklist...)
  1110  
  1111  	proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist, globalProj.Spec.NamespaceResourceWhitelist...)
  1112  	proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, globalProj.Spec.NamespaceResourceBlacklist...)
  1113  
  1114  	proj.Spec.SyncWindows = append(proj.Spec.SyncWindows, globalProj.Spec.SyncWindows...)
  1115  
  1116  	proj.Spec.SourceRepos = append(proj.Spec.SourceRepos, globalProj.Spec.SourceRepos...)
  1117  
  1118  	proj.Spec.Destinations = append(proj.Spec.Destinations, globalProj.Spec.Destinations...)
  1119  
  1120  	return proj
  1121  }
  1122  
  1123  func GenerateSpecIsDifferentErrorMessage(entity string, a, b any) string {
  1124  	basicMsg := fmt.Sprintf("existing %s spec is different; use upsert flag to force update", entity)
  1125  	difference, _ := GetDifferentPathsBetweenStructs(a, b)
  1126  	if len(difference) == 0 {
  1127  		return basicMsg
  1128  	}
  1129  	return fmt.Sprintf("%s; difference in keys %q", basicMsg, strings.Join(difference, ","))
  1130  }
  1131  
  1132  func GetDifferentPathsBetweenStructs(a, b any) ([]string, error) {
  1133  	var difference []string
  1134  	changelog, err := diff.Diff(a, b)
  1135  	if err != nil {
  1136  		return nil, fmt.Errorf("error during diff: %w", err)
  1137  	}
  1138  	for _, changeItem := range changelog {
  1139  		difference = append(difference, changeItem.Path...)
  1140  	}
  1141  	return difference, nil
  1142  }
  1143  
  1144  // parseName will split the qualified name into its components, which are separated by the delimiter.
  1145  // If delimiter is not contained in the string qualifiedName then returned namespace is defaultNs.
  1146  func parseName(qualifiedName string, defaultNs string, delim string) (name string, namespace string) {
  1147  	t := strings.SplitN(qualifiedName, delim, 2)
  1148  	if len(t) == 2 {
  1149  		namespace = t[0]
  1150  		name = t[1]
  1151  	} else {
  1152  		namespace = defaultNs
  1153  		name = t[0]
  1154  	}
  1155  	return
  1156  }
  1157  
  1158  // ParseAppNamespacedName parses a namespaced name in the format namespace/name
  1159  // and returns the components. If name wasn't namespaced, defaultNs will be
  1160  // returned as namespace component.
  1161  func ParseFromQualifiedName(qualifiedAppName string, defaultNs string) (appName string, appNamespace string) {
  1162  	return parseName(qualifiedAppName, defaultNs, "/")
  1163  }
  1164  
  1165  // ParseInstanceName parses a namespaced name in the format namespace_name
  1166  // and returns the components. If name wasn't namespaced, defaultNs will be
  1167  // returned as namespace component.
  1168  func ParseInstanceName(appName string, defaultNs string) (string, string) {
  1169  	return parseName(appName, defaultNs, "_")
  1170  }
  1171  
  1172  // AppInstanceName returns the value to be used for app instance labels from
  1173  // the combination of appName, appNs and defaultNs.
  1174  func AppInstanceName(appName, appNs, defaultNs string) string {
  1175  	if appNs == "" || appNs == defaultNs {
  1176  		return appName
  1177  	}
  1178  	return appNs + "_" + appName
  1179  }
  1180  
  1181  // InstanceNameFromQualified returns the value to be used for app
  1182  func InstanceNameFromQualified(name string, defaultNs string) string {
  1183  	appName, appNs := ParseFromQualifiedName(name, defaultNs)
  1184  	return AppInstanceName(appName, appNs, defaultNs)
  1185  }
  1186  
  1187  // ErrProjectNotPermitted returns an error to indicate that an application
  1188  // identified by appName and appNamespace is not allowed to use the project
  1189  // identified by projName.
  1190  func ErrProjectNotPermitted(appName, appNamespace, projName string) error {
  1191  	return fmt.Errorf("application '%s' in namespace '%s' is not permitted to use project '%s'", appName, appNamespace, projName)
  1192  }
  1193  
  1194  // IsValidPodName checks that a podName is valid
  1195  func IsValidPodName(name string) bool {
  1196  	// https://github.com/kubernetes/kubernetes/blob/976a940f4a4e84fe814583848f97b9aafcdb083f/pkg/apis/core/validation/validation.go#L241
  1197  	validationErrors := apimachineryvalidation.NameIsDNSSubdomain(name, false)
  1198  	return len(validationErrors) == 0
  1199  }
  1200  
  1201  // IsValidAppName checks if the name can be used as application name
  1202  func IsValidAppName(name string) bool {
  1203  	// app names have the same rules as pods.
  1204  	return IsValidPodName(name)
  1205  }
  1206  
  1207  // IsValidProjectName checks if the name can be used as project name
  1208  func IsValidProjectName(name string) bool {
  1209  	// project names have the same rules as pods.
  1210  	return IsValidPodName(name)
  1211  }
  1212  
  1213  // IsValidNamespaceName checks that a namespace name is valid
  1214  func IsValidNamespaceName(name string) bool {
  1215  	// https://github.com/kubernetes/kubernetes/blob/976a940f4a4e84fe814583848f97b9aafcdb083f/pkg/apis/core/validation/validation.go#L262
  1216  	validationErrors := apimachineryvalidation.ValidateNamespaceName(name, false)
  1217  	return len(validationErrors) == 0
  1218  }
  1219  
  1220  // IsValidContainerName checks that a containerName is valid
  1221  func IsValidContainerName(name string) bool {
  1222  	// https://github.com/kubernetes/kubernetes/blob/53a9d106c4aabcd550cc32ae4e8004f32fb0ae7b/pkg/api/validation/validation.go#L280
  1223  	validationErrors := apimachineryvalidation.NameIsDNSLabel(name, false)
  1224  	return len(validationErrors) == 0
  1225  }
  1226  
  1227  // GetAppEventLabels returns a map of labels to add to a K8s event.
  1228  // The Application and its AppProject labels are compared against the `resource.includeEventLabelKeys` key in argocd-cm.
  1229  // If matched, the corresponding labels are returned to be added to the generated event. In case of a conflict
  1230  // between labels on the Application and AppProject, the Application label values are prioritized and added to the event.
  1231  // Furthermore, labels specified in `resource.excludeEventLabelKeys` in argocd-cm are removed from the event labels, if they were included.
  1232  func GetAppEventLabels(ctx context.Context, app *argoappv1.Application, projLister applicationsv1.AppProjectLister, ns string, settingsManager *settings.SettingsManager, db db.ArgoDB) map[string]string {
  1233  	eventLabels := make(map[string]string)
  1234  
  1235  	// Get all app & app-project labels
  1236  	labels := app.Labels
  1237  	if labels == nil {
  1238  		labels = make(map[string]string)
  1239  	}
  1240  	proj, err := GetAppProject(ctx, app, projLister, ns, settingsManager, db)
  1241  	if err == nil {
  1242  		for k, v := range proj.Labels {
  1243  			_, found := labels[k]
  1244  			if !found {
  1245  				labels[k] = v
  1246  			}
  1247  		}
  1248  	} else {
  1249  		log.Warn(err)
  1250  	}
  1251  
  1252  	// Filter out event labels to include
  1253  	inKeys := settingsManager.GetIncludeEventLabelKeys()
  1254  	for k, v := range labels {
  1255  		found := glob.MatchStringInList(inKeys, k, glob.GLOB)
  1256  		if found {
  1257  			eventLabels[k] = v
  1258  		}
  1259  	}
  1260  
  1261  	// Remove excluded event labels
  1262  	exKeys := settingsManager.GetExcludeEventLabelKeys()
  1263  	for k := range eventLabels {
  1264  		found := glob.MatchStringInList(exKeys, k, glob.GLOB)
  1265  		if found {
  1266  			delete(eventLabels, k)
  1267  		}
  1268  	}
  1269  
  1270  	return eventLabels
  1271  }