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 }