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

     1  package controller
     2  
     3  import (
     4  	"context"
     5  	stderrors "errors"
     6  	"fmt"
     7  	"os"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    13  
    14  	cdcommon "github.com/argoproj/argo-cd/v3/common"
    15  
    16  	gitopsDiff "github.com/argoproj/gitops-engine/pkg/diff"
    17  	"github.com/argoproj/gitops-engine/pkg/sync"
    18  	"github.com/argoproj/gitops-engine/pkg/sync/common"
    19  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    20  	jsonpatch "github.com/evanphx/json-patch"
    21  	log "github.com/sirupsen/logrus"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apimachinery/pkg/util/managedfields"
    26  	"k8s.io/client-go/kubernetes/scheme"
    27  	"k8s.io/client-go/rest"
    28  	"k8s.io/kubectl/pkg/util/openapi"
    29  
    30  	"github.com/argoproj/argo-cd/v3/controller/metrics"
    31  	"github.com/argoproj/argo-cd/v3/controller/syncid"
    32  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    33  	applog "github.com/argoproj/argo-cd/v3/util/app/log"
    34  	"github.com/argoproj/argo-cd/v3/util/argo"
    35  	"github.com/argoproj/argo-cd/v3/util/argo/diff"
    36  	"github.com/argoproj/argo-cd/v3/util/glob"
    37  	kubeutil "github.com/argoproj/argo-cd/v3/util/kube"
    38  	logutils "github.com/argoproj/argo-cd/v3/util/log"
    39  	"github.com/argoproj/argo-cd/v3/util/lua"
    40  )
    41  
    42  const (
    43  	// EnvVarSyncWaveDelay is an environment variable which controls the delay in seconds between
    44  	// each sync-wave
    45  	EnvVarSyncWaveDelay = "ARGOCD_SYNC_WAVE_DELAY"
    46  
    47  	// serviceAccountDisallowedCharSet contains the characters that are not allowed to be present
    48  	// in a DefaultServiceAccount configured for a DestinationServiceAccount
    49  	serviceAccountDisallowedCharSet = "!*[]{}\\/"
    50  )
    51  
    52  func (m *appStateManager) getOpenAPISchema(server *v1alpha1.Cluster) (openapi.Resources, error) {
    53  	cluster, err := m.liveStateCache.GetClusterCache(server)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	return cluster.GetOpenAPISchema(), nil
    58  }
    59  
    60  func (m *appStateManager) getGVKParser(server *v1alpha1.Cluster) (*managedfields.GvkParser, error) {
    61  	cluster, err := m.liveStateCache.GetClusterCache(server)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	return cluster.GetGVKParser(), nil
    66  }
    67  
    68  // getServerSideDiffDryRunApplier will return the kubectl implementation of the KubeApplier
    69  // interface that provides functionality to dry run apply kubernetes resources. Returns a
    70  // cleanup function that must be called to remove the generated kube config for this
    71  // server.
    72  func (m *appStateManager) getServerSideDiffDryRunApplier(cluster *v1alpha1.Cluster) (gitopsDiff.KubeApplier, func(), error) {
    73  	clusterCache, err := m.liveStateCache.GetClusterCache(cluster)
    74  	if err != nil {
    75  		return nil, nil, fmt.Errorf("error getting cluster cache: %w", err)
    76  	}
    77  
    78  	rawConfig, err := cluster.RawRestConfig()
    79  	if err != nil {
    80  		return nil, nil, fmt.Errorf("error getting cluster REST config: %w", err)
    81  	}
    82  	ops, cleanup, err := kubeutil.ManageServerSideDiffDryRuns(rawConfig, clusterCache.GetOpenAPISchema(), m.onKubectlRun)
    83  	if err != nil {
    84  		return nil, nil, fmt.Errorf("error creating kubectl ResourceOperations: %w", err)
    85  	}
    86  	return ops, cleanup, nil
    87  }
    88  
    89  func NewOperationState(operation v1alpha1.Operation) *v1alpha1.OperationState {
    90  	return &v1alpha1.OperationState{
    91  		Phase:     common.OperationRunning,
    92  		Operation: operation,
    93  		StartedAt: metav1.Now(),
    94  	}
    95  }
    96  
    97  func newSyncOperationResult(app *v1alpha1.Application, op v1alpha1.SyncOperation) *v1alpha1.SyncOperationResult {
    98  	syncRes := &v1alpha1.SyncOperationResult{}
    99  
   100  	if len(op.Sources) > 0 || op.Source != nil {
   101  		// specific source specified in the SyncOperation
   102  		if op.Source != nil {
   103  			syncRes.Source = *op.Source
   104  		}
   105  		syncRes.Sources = op.Sources
   106  	} else {
   107  		// normal sync case, get sources from the spec
   108  		syncRes.Sources = app.Spec.Sources
   109  		syncRes.Source = app.Spec.GetSource()
   110  	}
   111  
   112  	// Sync requests might be requested with ambiguous revisions (e.g. master, HEAD, v1.2.3).
   113  	// This can change meaning when resuming operations (e.g a hook sync). After calculating a
   114  	// concrete git commit SHA, the revision of the SyncOperationResult will be updated with the SHA
   115  	syncRes.Revision = op.Revision
   116  	syncRes.Revisions = op.Revisions
   117  	return syncRes
   118  }
   119  
   120  func (m *appStateManager) SyncAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, state *v1alpha1.OperationState) {
   121  	syncId, err := syncid.Generate()
   122  	if err != nil {
   123  		state.Phase = common.OperationError
   124  		state.Message = fmt.Sprintf("Failed to generate sync ID: %v", err)
   125  		return
   126  	}
   127  	logEntry := log.WithFields(applog.GetAppLogFields(app)).WithField("syncId", syncId)
   128  
   129  	if state.Operation.Sync == nil {
   130  		state.Phase = common.OperationError
   131  		state.Message = "Invalid operation request: no operation specified"
   132  		return
   133  	}
   134  
   135  	syncOp := *state.Operation.Sync
   136  
   137  	if state.SyncResult == nil {
   138  		state.SyncResult = newSyncOperationResult(app, syncOp)
   139  	}
   140  
   141  	if isBlocked, err := syncWindowPreventsSync(app, project); isBlocked {
   142  		// If the operation is currently running, simply let the user know the sync is blocked by a current sync window
   143  		if state.Phase == common.OperationRunning {
   144  			state.Message = "Sync operation blocked by sync window"
   145  			if err != nil {
   146  				state.Message = fmt.Sprintf("%s: %v", state.Message, err)
   147  			}
   148  		}
   149  		return
   150  	}
   151  
   152  	revisions := state.SyncResult.Revisions
   153  	sources := state.SyncResult.Sources
   154  	isMultiSourceSync := len(sources) > 0
   155  	if !isMultiSourceSync {
   156  		sources = []v1alpha1.ApplicationSource{state.SyncResult.Source}
   157  		revisions = []string{state.SyncResult.Revision}
   158  	}
   159  
   160  	// ignore error if CompareStateRepoError, this shouldn't happen as noRevisionCache is true
   161  	compareResult, err := m.CompareAppState(app, project, revisions, sources, false, true, syncOp.Manifests, isMultiSourceSync)
   162  	if err != nil && !stderrors.Is(err, ErrCompareStateRepo) {
   163  		state.Phase = common.OperationError
   164  		state.Message = err.Error()
   165  		return
   166  	}
   167  
   168  	// We are now guaranteed to have a concrete commit SHA. Save this in the sync result revision so that we remember
   169  	// what we should be syncing to when resuming operations.
   170  	state.SyncResult.Revision = compareResult.syncStatus.Revision
   171  	state.SyncResult.Revisions = compareResult.syncStatus.Revisions
   172  
   173  	// validates if it should fail the sync on that revision if it finds shared resources
   174  	hasSharedResource, sharedResourceMessage := hasSharedResourceCondition(app)
   175  	if syncOp.SyncOptions.HasOption("FailOnSharedResource=true") && hasSharedResource {
   176  		state.Phase = common.OperationFailed
   177  		state.Message = "Shared resource found: " + sharedResourceMessage
   178  		return
   179  	}
   180  
   181  	// If there are any comparison or spec errors error conditions do not perform the operation
   182  	if errConditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{
   183  		v1alpha1.ApplicationConditionComparisonError:  true,
   184  		v1alpha1.ApplicationConditionInvalidSpecError: true,
   185  	}); len(errConditions) > 0 {
   186  		state.Phase = common.OperationError
   187  		state.Message = argo.FormatAppConditions(errConditions)
   188  		return
   189  	}
   190  
   191  	destCluster, err := argo.GetDestinationCluster(context.Background(), app.Spec.Destination, m.db)
   192  	if err != nil {
   193  		state.Phase = common.OperationError
   194  		state.Message = fmt.Sprintf("Failed to get destination cluster: %v", err)
   195  		return
   196  	}
   197  
   198  	rawConfig, err := destCluster.RawRestConfig()
   199  	if err != nil {
   200  		state.Phase = common.OperationError
   201  		state.Message = err.Error()
   202  		return
   203  	}
   204  
   205  	clusterRESTConfig, err := destCluster.RESTConfig()
   206  	if err != nil {
   207  		state.Phase = common.OperationError
   208  		state.Message = err.Error()
   209  		return
   210  	}
   211  	restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clusterRESTConfig)
   212  
   213  	resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
   214  	if err != nil {
   215  		state.Phase = common.OperationError
   216  		state.Message = fmt.Sprintf("Failed to load resource overrides: %v", err)
   217  		return
   218  	}
   219  
   220  	initialResourcesRes := make([]common.ResourceSyncResult, len(state.SyncResult.Resources))
   221  	for i, res := range state.SyncResult.Resources {
   222  		key := kube.ResourceKey{Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name}
   223  		initialResourcesRes[i] = common.ResourceSyncResult{
   224  			ResourceKey: key,
   225  			Message:     res.Message,
   226  			Status:      res.Status,
   227  			HookPhase:   res.HookPhase,
   228  			HookType:    res.HookType,
   229  			SyncPhase:   res.SyncPhase,
   230  			Version:     res.Version,
   231  			Images:      res.Images,
   232  			Order:       i + 1,
   233  		}
   234  	}
   235  
   236  	prunePropagationPolicy := metav1.DeletePropagationForeground
   237  	switch {
   238  	case syncOp.SyncOptions.HasOption("PrunePropagationPolicy=background"):
   239  		prunePropagationPolicy = metav1.DeletePropagationBackground
   240  	case syncOp.SyncOptions.HasOption("PrunePropagationPolicy=foreground"):
   241  		prunePropagationPolicy = metav1.DeletePropagationForeground
   242  	case syncOp.SyncOptions.HasOption("PrunePropagationPolicy=orphan"):
   243  		prunePropagationPolicy = metav1.DeletePropagationOrphan
   244  	}
   245  
   246  	clientSideApplyManager := common.DefaultClientSideApplyMigrationManager
   247  	// Check for custom field manager from application annotation
   248  	if managerValue := app.GetAnnotation(cdcommon.AnnotationClientSideApplyMigrationManager); managerValue != "" {
   249  		clientSideApplyManager = managerValue
   250  	}
   251  
   252  	openAPISchema, err := m.getOpenAPISchema(destCluster)
   253  	if err != nil {
   254  		state.Phase = common.OperationError
   255  		state.Message = fmt.Sprintf("failed to load openAPISchema: %v", err)
   256  		return
   257  	}
   258  
   259  	reconciliationResult := compareResult.reconciliationResult
   260  
   261  	// if RespectIgnoreDifferences is enabled, it should normalize the target
   262  	// resources which in this case applies the live values in the configured
   263  	// ignore differences fields.
   264  	if syncOp.SyncOptions.HasOption("RespectIgnoreDifferences=true") {
   265  		patchedTargets, err := normalizeTargetResources(compareResult)
   266  		if err != nil {
   267  			state.Phase = common.OperationError
   268  			state.Message = fmt.Sprintf("Failed to normalize target resources: %s", err)
   269  			return
   270  		}
   271  		reconciliationResult.Target = patchedTargets
   272  	}
   273  
   274  	installationID, err := m.settingsMgr.GetInstallationID()
   275  	if err != nil {
   276  		log.Errorf("Could not get installation ID: %v", err)
   277  		return
   278  	}
   279  	trackingMethod, err := m.settingsMgr.GetTrackingMethod()
   280  	if err != nil {
   281  		log.Errorf("Could not get trackingMethod: %v", err)
   282  		return
   283  	}
   284  
   285  	impersonationEnabled, err := m.settingsMgr.IsImpersonationEnabled()
   286  	if err != nil {
   287  		log.Errorf("could not get impersonation feature flag: %v", err)
   288  		return
   289  	}
   290  	if impersonationEnabled {
   291  		serviceAccountToImpersonate, err := deriveServiceAccountToImpersonate(project, app, destCluster)
   292  		if err != nil {
   293  			state.Phase = common.OperationError
   294  			state.Message = fmt.Sprintf("failed to find a matching service account to impersonate: %v", err)
   295  			return
   296  		}
   297  		logEntry = logEntry.WithFields(log.Fields{"impersonationEnabled": "true", "serviceAccount": serviceAccountToImpersonate})
   298  		// set the impersonation headers.
   299  		rawConfig.Impersonate = rest.ImpersonationConfig{
   300  			UserName: serviceAccountToImpersonate,
   301  		}
   302  		restConfig.Impersonate = rest.ImpersonationConfig{
   303  			UserName: serviceAccountToImpersonate,
   304  		}
   305  	}
   306  
   307  	opts := []sync.SyncOpt{
   308  		sync.WithLogr(logutils.NewLogrusLogger(logEntry)),
   309  		sync.WithHealthOverride(lua.ResourceHealthOverrides(resourceOverrides)),
   310  		sync.WithPermissionValidator(func(un *unstructured.Unstructured, res *metav1.APIResource) error {
   311  			if !project.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), res.Namespaced) {
   312  				return fmt.Errorf("resource %s:%s is not permitted in project %s", un.GroupVersionKind().Group, un.GroupVersionKind().Kind, project.Name)
   313  			}
   314  			if res.Namespaced {
   315  				permitted, err := project.IsDestinationPermitted(destCluster, un.GetNamespace(), func(project string) ([]*v1alpha1.Cluster, error) {
   316  					return m.db.GetProjectClusters(context.TODO(), project)
   317  				})
   318  				if err != nil {
   319  					return err
   320  				}
   321  
   322  				if !permitted {
   323  					return fmt.Errorf("namespace %v is not permitted in project '%s'", un.GetNamespace(), project.Name)
   324  				}
   325  			}
   326  			return nil
   327  		}),
   328  		sync.WithOperationSettings(syncOp.DryRun, syncOp.Prune, syncOp.SyncStrategy.Force(), syncOp.IsApplyStrategy() || len(syncOp.Resources) > 0),
   329  		sync.WithInitialState(state.Phase, state.Message, initialResourcesRes, state.StartedAt),
   330  		sync.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool {
   331  			return (len(syncOp.Resources) == 0 ||
   332  				isPostDeleteHook(target) ||
   333  				argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)) &&
   334  				m.isSelfReferencedObj(live, target, app.GetName(), v1alpha1.TrackingMethod(trackingMethod), installationID)
   335  		}),
   336  		sync.WithManifestValidation(!syncOp.SyncOptions.HasOption(common.SyncOptionsDisableValidation)),
   337  		sync.WithSyncWaveHook(delayBetweenSyncWaves),
   338  		sync.WithPruneLast(syncOp.SyncOptions.HasOption(common.SyncOptionPruneLast)),
   339  		sync.WithResourceModificationChecker(syncOp.SyncOptions.HasOption("ApplyOutOfSyncOnly=true"), compareResult.diffResultList),
   340  		sync.WithPrunePropagationPolicy(&prunePropagationPolicy),
   341  		sync.WithReplace(syncOp.SyncOptions.HasOption(common.SyncOptionReplace)),
   342  		sync.WithServerSideApply(syncOp.SyncOptions.HasOption(common.SyncOptionServerSideApply)),
   343  		sync.WithServerSideApplyManager(cdcommon.ArgoCDSSAManager),
   344  		sync.WithClientSideApplyMigration(
   345  			!syncOp.SyncOptions.HasOption(common.SyncOptionDisableClientSideApplyMigration),
   346  			clientSideApplyManager,
   347  		),
   348  		sync.WithPruneConfirmed(app.IsDeletionConfirmed(state.StartedAt.Time)),
   349  		sync.WithSkipDryRunOnMissingResource(syncOp.SyncOptions.HasOption(common.SyncOptionSkipDryRunOnMissingResource)),
   350  	}
   351  
   352  	if syncOp.SyncOptions.HasOption("CreateNamespace=true") {
   353  		opts = append(opts, sync.WithNamespaceModifier(syncNamespace(app.Spec.SyncPolicy)))
   354  	}
   355  
   356  	syncCtx, cleanup, err := sync.NewSyncContext(
   357  		compareResult.syncStatus.Revision,
   358  		reconciliationResult,
   359  		restConfig,
   360  		rawConfig,
   361  		m.kubectl,
   362  		app.Spec.Destination.Namespace,
   363  		openAPISchema,
   364  		opts...,
   365  	)
   366  	if err != nil {
   367  		state.Phase = common.OperationError
   368  		state.Message = fmt.Sprintf("failed to initialize sync context: %v", err)
   369  		return
   370  	}
   371  
   372  	defer cleanup()
   373  
   374  	start := time.Now()
   375  
   376  	if state.Phase == common.OperationTerminating {
   377  		syncCtx.Terminate()
   378  	} else {
   379  		syncCtx.Sync()
   380  	}
   381  	var resState []common.ResourceSyncResult
   382  	state.Phase, state.Message, resState = syncCtx.GetState()
   383  	state.SyncResult.Resources = nil
   384  
   385  	if app.Spec.SyncPolicy != nil {
   386  		state.SyncResult.ManagedNamespaceMetadata = app.Spec.SyncPolicy.ManagedNamespaceMetadata
   387  	}
   388  
   389  	var apiVersion []kube.APIResourceInfo
   390  	for _, res := range resState {
   391  		augmentedMsg, err := argo.AugmentSyncMsg(res, func() ([]kube.APIResourceInfo, error) {
   392  			if apiVersion == nil {
   393  				_, apiVersion, err = m.liveStateCache.GetVersionsInfo(destCluster)
   394  				if err != nil {
   395  					return nil, fmt.Errorf("failed to get version info from the target cluster %q", destCluster.Server)
   396  				}
   397  			}
   398  			return apiVersion, nil
   399  		})
   400  
   401  		if err != nil {
   402  			log.Errorf("using the original message since: %v", err)
   403  		} else {
   404  			res.Message = augmentedMsg
   405  		}
   406  
   407  		state.SyncResult.Resources = append(state.SyncResult.Resources, &v1alpha1.ResourceResult{
   408  			HookType:  res.HookType,
   409  			Group:     res.ResourceKey.Group,
   410  			Kind:      res.ResourceKey.Kind,
   411  			Namespace: res.ResourceKey.Namespace,
   412  			Name:      res.ResourceKey.Name,
   413  			Version:   res.Version,
   414  			SyncPhase: res.SyncPhase,
   415  			HookPhase: res.HookPhase,
   416  			Status:    res.Status,
   417  			Message:   res.Message,
   418  			Images:    res.Images,
   419  		})
   420  	}
   421  
   422  	logEntry.WithField("duration", time.Since(start)).Info("sync/terminate complete")
   423  
   424  	if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() {
   425  		err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, compareResult.syncStatus.ComparedTo.Source, compareResult.syncStatus.Revisions, compareResult.syncStatus.ComparedTo.Sources, isMultiSourceSync, state.StartedAt, state.Operation.InitiatedBy)
   426  		if err != nil {
   427  			state.Phase = common.OperationError
   428  			state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
   429  		}
   430  	}
   431  }
   432  
   433  // normalizeTargetResources modifies target resources to ensure ignored fields are not touched during synchronization:
   434  //   - applies normalization to the target resources based on the live resources
   435  //   - copies ignored fields from the matching live resources: apply normalizer to the live resource,
   436  //     calculates the patch performed by normalizer and applies the patch to the target resource
   437  func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructured, error) {
   438  	// normalize live and target resources
   439  	normalized, err := diff.Normalize(cr.reconciliationResult.Live, cr.reconciliationResult.Target, cr.diffConfig)
   440  	if err != nil {
   441  		return nil, err
   442  	}
   443  	patchedTargets := []*unstructured.Unstructured{}
   444  	for idx, live := range cr.reconciliationResult.Live {
   445  		normalizedTarget := normalized.Targets[idx]
   446  		if normalizedTarget == nil {
   447  			patchedTargets = append(patchedTargets, nil)
   448  			continue
   449  		}
   450  		originalTarget := cr.reconciliationResult.Target[idx]
   451  		if live == nil {
   452  			patchedTargets = append(patchedTargets, originalTarget)
   453  			continue
   454  		}
   455  
   456  		var lookupPatchMeta *strategicpatch.PatchMetaFromStruct
   457  		versionedObject, err := scheme.Scheme.New(normalizedTarget.GroupVersionKind())
   458  		if err == nil {
   459  			meta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
   460  			if err != nil {
   461  				return nil, err
   462  			}
   463  			lookupPatchMeta = &meta
   464  		}
   465  
   466  		livePatch, err := getMergePatch(normalized.Lives[idx], live, lookupPatchMeta)
   467  		if err != nil {
   468  			return nil, err
   469  		}
   470  
   471  		normalizedTarget, err = applyMergePatch(normalizedTarget, livePatch, versionedObject)
   472  		if err != nil {
   473  			return nil, err
   474  		}
   475  
   476  		patchedTargets = append(patchedTargets, normalizedTarget)
   477  	}
   478  	return patchedTargets, nil
   479  }
   480  
   481  // getMergePatch calculates and returns the patch between the original and the
   482  // modified unstructures.
   483  func getMergePatch(original, modified *unstructured.Unstructured, lookupPatchMeta *strategicpatch.PatchMetaFromStruct) ([]byte, error) {
   484  	originalJSON, err := original.MarshalJSON()
   485  	if err != nil {
   486  		return nil, err
   487  	}
   488  	modifiedJSON, err := modified.MarshalJSON()
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  	if lookupPatchMeta != nil {
   493  		return strategicpatch.CreateThreeWayMergePatch(modifiedJSON, modifiedJSON, originalJSON, lookupPatchMeta, true)
   494  	}
   495  
   496  	return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
   497  }
   498  
   499  // applyMergePatch will apply the given patch in the obj and return the patched
   500  // unstructure.
   501  func applyMergePatch(obj *unstructured.Unstructured, patch []byte, versionedObject any) (*unstructured.Unstructured, error) {
   502  	originalJSON, err := obj.MarshalJSON()
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  	var patchedJSON []byte
   507  	if versionedObject == nil {
   508  		patchedJSON, err = jsonpatch.MergePatch(originalJSON, patch)
   509  	} else {
   510  		patchedJSON, err = strategicpatch.StrategicMergePatch(originalJSON, patch, versionedObject)
   511  	}
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  
   516  	patchedObj := &unstructured.Unstructured{}
   517  	_, _, err = unstructured.UnstructuredJSONScheme.Decode(patchedJSON, nil, patchedObj)
   518  	if err != nil {
   519  		return nil, err
   520  	}
   521  	return patchedObj, nil
   522  }
   523  
   524  // hasSharedResourceCondition will check if the Application has any resource that has already
   525  // been synced by another Application. If the resource is found in another Application it returns
   526  // true along with a human readable message of which specific resource has this condition.
   527  func hasSharedResourceCondition(app *v1alpha1.Application) (bool, string) {
   528  	for _, condition := range app.Status.Conditions {
   529  		if condition.Type == v1alpha1.ApplicationConditionSharedResourceWarning {
   530  			return true, condition.Message
   531  		}
   532  	}
   533  	return false, ""
   534  }
   535  
   536  // delayBetweenSyncWaves is a gitops-engine SyncWaveHook which introduces an artificial delay
   537  // between each sync wave. We introduce an artificial delay in order give other controllers a
   538  // _chance_ to react to the spec change that we just applied. This is important because without
   539  // this, Argo CD will likely assess resource health too quickly (against the stale object), causing
   540  // hooks to fire prematurely. See: https://github.com/argoproj/argo-cd/issues/4669.
   541  // Note, this is not foolproof, since a proper fix would require the CRD record
   542  // status.observedGeneration coupled with a health.lua that verifies
   543  // status.observedGeneration == metadata.generation
   544  func delayBetweenSyncWaves(_ common.SyncPhase, _ int, finalWave bool) error {
   545  	if !finalWave {
   546  		delaySec := 2
   547  		if delaySecStr := os.Getenv(EnvVarSyncWaveDelay); delaySecStr != "" {
   548  			if val, err := strconv.Atoi(delaySecStr); err == nil {
   549  				delaySec = val
   550  			}
   551  		}
   552  		duration := time.Duration(delaySec) * time.Second
   553  		time.Sleep(duration)
   554  	}
   555  	return nil
   556  }
   557  
   558  func syncWindowPreventsSync(app *v1alpha1.Application, proj *v1alpha1.AppProject) (bool, error) {
   559  	window := proj.Spec.SyncWindows.Matches(app)
   560  	isManual := false
   561  	if app.Status.OperationState != nil {
   562  		isManual = !app.Status.OperationState.Operation.InitiatedBy.Automated
   563  	}
   564  	canSync, err := window.CanSync(isManual)
   565  	if err != nil {
   566  		// prevents sync because sync window has an error
   567  		return true, err
   568  	}
   569  	return !canSync, nil
   570  }
   571  
   572  // deriveServiceAccountToImpersonate determines the service account to be used for impersonation for the sync operation.
   573  // The returned service account will be fully qualified including namespace and the service account name in the format system:serviceaccount:<namespace>:<service_account>
   574  func deriveServiceAccountToImpersonate(project *v1alpha1.AppProject, application *v1alpha1.Application, destCluster *v1alpha1.Cluster) (string, error) {
   575  	// spec.Destination.Namespace is optional. If not specified, use the Application's
   576  	// namespace
   577  	serviceAccountNamespace := application.Spec.Destination.Namespace
   578  	if serviceAccountNamespace == "" {
   579  		serviceAccountNamespace = application.Namespace
   580  	}
   581  	// Loop through the destinationServiceAccounts and see if there is any destination that is a candidate.
   582  	// if so, return the service account specified for that destination.
   583  	for _, item := range project.Spec.DestinationServiceAccounts {
   584  		dstServerMatched, err := glob.MatchWithError(item.Server, destCluster.Server)
   585  		if err != nil {
   586  			return "", fmt.Errorf("invalid glob pattern for destination server: %w", err)
   587  		}
   588  		dstNamespaceMatched, err := glob.MatchWithError(item.Namespace, application.Spec.Destination.Namespace)
   589  		if err != nil {
   590  			return "", fmt.Errorf("invalid glob pattern for destination namespace: %w", err)
   591  		}
   592  		if dstServerMatched && dstNamespaceMatched {
   593  			if strings.Trim(item.DefaultServiceAccount, " ") == "" || strings.ContainsAny(item.DefaultServiceAccount, serviceAccountDisallowedCharSet) {
   594  				return "", fmt.Errorf("default service account contains invalid chars '%s'", item.DefaultServiceAccount)
   595  			} else if strings.Contains(item.DefaultServiceAccount, ":") {
   596  				// service account is specified along with its namespace.
   597  				return "system:serviceaccount:" + item.DefaultServiceAccount, nil
   598  			}
   599  			// service account needs to be prefixed with a namespace
   600  			return fmt.Sprintf("system:serviceaccount:%s:%s", serviceAccountNamespace, item.DefaultServiceAccount), nil
   601  		}
   602  	}
   603  	// if there is no match found in the AppProject.Spec.DestinationServiceAccounts, use the default service account of the destination namespace.
   604  	return "", fmt.Errorf("no matching service account found for destination server %s and namespace %s", application.Spec.Destination.Server, serviceAccountNamespace)
   605  }