2018-07-07 07:54:06 +00:00
package controller
import (
"context"
2024-12-30 08:56:41 +00:00
stderrors "errors"
2018-07-07 07:54:06 +00:00
"fmt"
2020-11-02 20:17:43 +00:00
"os"
"strconv"
2024-09-04 18:18:47 +00:00
"strings"
2019-07-24 22:00:40 +00:00
"time"
2018-07-07 07:54:06 +00:00
2024-04-03 22:56:59 +00:00
"k8s.io/apimachinery/pkg/util/strategicpatch"
2022-12-16 20:47:08 +00:00
2025-01-10 21:14:00 +00:00
cdcommon "github.com/argoproj/argo-cd/v3/common"
2024-06-10 13:18:12 +00:00
2026-02-12 14:29:40 +00:00
gitopsDiff "github.com/argoproj/argo-cd/gitops-engine/pkg/diff"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
2022-01-13 21:00:17 +00:00
jsonpatch "github.com/evanphx/json-patch"
2018-07-07 07:54:06 +00:00
log "github.com/sirupsen/logrus"
2024-12-31 08:34:11 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2018-07-07 07:54:06 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2019-12-26 22:08:14 +00:00
"k8s.io/apimachinery/pkg/runtime/schema"
2022-05-31 17:21:22 +00:00
"k8s.io/apimachinery/pkg/util/managedfields"
2024-04-03 22:56:59 +00:00
"k8s.io/client-go/kubernetes/scheme"
2024-09-04 18:18:47 +00:00
"k8s.io/client-go/rest"
2021-06-17 19:06:18 +00:00
"k8s.io/kubectl/pkg/util/openapi"
2018-07-07 07:54:06 +00:00
2025-01-10 21:14:00 +00:00
"github.com/argoproj/argo-cd/v3/controller/metrics"
2025-06-18 20:37:57 +00:00
"github.com/argoproj/argo-cd/v3/controller/syncid"
2025-01-10 21:14:00 +00:00
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
2025-05-20 19:48:09 +00:00
applog "github.com/argoproj/argo-cd/v3/util/app/log"
2025-01-10 21:14:00 +00:00
"github.com/argoproj/argo-cd/v3/util/argo"
"github.com/argoproj/argo-cd/v3/util/argo/diff"
"github.com/argoproj/argo-cd/v3/util/glob"
2025-02-07 17:26:03 +00:00
kubeutil "github.com/argoproj/argo-cd/v3/util/kube"
2025-01-10 21:14:00 +00:00
logutils "github.com/argoproj/argo-cd/v3/util/log"
"github.com/argoproj/argo-cd/v3/util/lua"
2019-07-24 22:00:40 +00:00
)
2020-11-02 20:17:43 +00:00
const (
// EnvVarSyncWaveDelay is an environment variable which controls the delay in seconds between
// each sync-wave
EnvVarSyncWaveDelay = "ARGOCD_SYNC_WAVE_DELAY"
2024-10-03 14:37:50 +00:00
// serviceAccountDisallowedCharSet contains the characters that are not allowed to be present
// in a DefaultServiceAccount configured for a DestinationServiceAccount
serviceAccountDisallowedCharSet = "!*[]{}\\/"
2020-11-02 20:17:43 +00:00
)
2025-01-13 18:15:42 +00:00
func ( m * appStateManager ) getOpenAPISchema ( server * v1alpha1 . Cluster ) ( openapi . Resources , error ) {
2021-06-17 19:06:18 +00:00
cluster , err := m . liveStateCache . GetClusterCache ( server )
if err != nil {
return nil , err
}
return cluster . GetOpenAPISchema ( ) , nil
}
2025-01-13 18:15:42 +00:00
func ( m * appStateManager ) getGVKParser ( server * v1alpha1 . Cluster ) ( * managedfields . GvkParser , error ) {
2022-05-31 17:21:22 +00:00
cluster , err := m . liveStateCache . GetClusterCache ( server )
if err != nil {
return nil , err
}
return cluster . GetGVKParser ( ) , nil
}
2025-02-07 17:26:03 +00:00
// getServerSideDiffDryRunApplier will return the kubectl implementation of the KubeApplier
// interface that provides functionality to dry run apply kubernetes resources. Returns a
2023-12-18 20:37:13 +00:00
// cleanup function that must be called to remove the generated kube config for this
// server.
2025-02-07 17:26:03 +00:00
func ( m * appStateManager ) getServerSideDiffDryRunApplier ( cluster * v1alpha1 . Cluster ) ( gitopsDiff . KubeApplier , func ( ) , error ) {
2025-01-13 18:15:42 +00:00
clusterCache , err := m . liveStateCache . GetClusterCache ( cluster )
2023-12-18 20:37:13 +00:00
if err != nil {
return nil , nil , fmt . Errorf ( "error getting cluster cache: %w" , err )
}
2024-10-06 14:55:26 +00:00
rawConfig , err := cluster . RawRestConfig ( )
if err != nil {
return nil , nil , fmt . Errorf ( "error getting cluster REST config: %w" , err )
}
2025-02-07 17:26:03 +00:00
ops , cleanup , err := kubeutil . ManageServerSideDiffDryRuns ( rawConfig , clusterCache . GetOpenAPISchema ( ) , m . onKubectlRun )
2023-12-18 20:37:13 +00:00
if err != nil {
return nil , nil , fmt . Errorf ( "error creating kubectl ResourceOperations: %w" , err )
}
return ops , cleanup , nil
}
2025-07-16 17:39:30 +00:00
func NewOperationState ( operation v1alpha1 . Operation ) * v1alpha1 . OperationState {
return & v1alpha1 . OperationState {
Phase : common . OperationRunning ,
Operation : operation ,
StartedAt : metav1 . Now ( ) ,
}
}
func newSyncOperationResult ( app * v1alpha1 . Application , op v1alpha1 . SyncOperation ) * v1alpha1 . SyncOperationResult {
syncRes := & v1alpha1 . SyncOperationResult { }
if len ( op . Sources ) > 0 || op . Source != nil {
// specific source specified in the SyncOperation
if op . Source != nil {
syncRes . Source = * op . Source
}
syncRes . Sources = op . Sources
} else {
// normal sync case, get sources from the spec
syncRes . Sources = app . Spec . Sources
syncRes . Source = app . Spec . GetSource ( )
}
2018-09-24 15:52:43 +00:00
// Sync requests might be requested with ambiguous revisions (e.g. master, HEAD, v1.2.3).
2018-07-07 07:54:06 +00:00
// This can change meaning when resuming operations (e.g a hook sync). After calculating a
2025-07-16 17:39:30 +00:00
// concrete git commit SHA, the revision of the SyncOperationResult will be updated with the SHA
syncRes . Revision = op . Revision
syncRes . Revisions = op . Revisions
return syncRes
}
func ( m * appStateManager ) SyncAppState ( app * v1alpha1 . Application , project * v1alpha1 . AppProject , state * v1alpha1 . OperationState ) {
2025-06-23 02:39:49 +00:00
syncId , err := syncid . Generate ( )
if err != nil {
state . Phase = common . OperationError
state . Message = fmt . Sprintf ( "Failed to generate sync ID: %v" , err )
return
}
logEntry := log . WithFields ( applog . GetAppLogFields ( app ) ) . WithField ( "syncId" , syncId )
2025-06-18 20:37:57 +00:00
2019-03-04 08:56:36 +00:00
if state . Operation . Sync == nil {
2025-07-16 17:39:30 +00:00
state . Phase = common . OperationError
2018-07-07 07:54:06 +00:00
state . Message = "Invalid operation request: no operation specified"
return
}
2021-10-20 15:06:41 +00:00
2025-07-16 17:39:30 +00:00
syncOp := * state . Operation . Sync
2020-05-15 17:01:18 +00:00
2025-07-16 17:39:30 +00:00
if state . SyncResult == nil {
state . SyncResult = newSyncOperationResult ( app , syncOp )
2019-03-04 08:56:36 +00:00
}
2018-07-07 07:54:06 +00:00
2025-07-16 17:39:30 +00:00
if isBlocked , err := syncWindowPreventsSync ( app , project ) ; isBlocked {
2025-07-03 17:16:58 +00:00
// If the operation is currently running, simply let the user know the sync is blocked by a current sync window
if state . Phase == common . OperationRunning {
state . Message = "Sync operation blocked by sync window"
if err != nil {
state . Message = fmt . Sprintf ( "%s: %v" , state . Message , err )
2024-10-12 17:32:46 +00:00
}
2024-03-26 12:45:35 +00:00
}
2025-07-03 17:16:58 +00:00
return
2019-12-26 22:08:14 +00:00
}
2025-07-16 17:39:30 +00:00
revisions := state . SyncResult . Revisions
sources := state . SyncResult . Sources
isMultiSourceSync := len ( sources ) > 0
if ! isMultiSourceSync {
sources = [ ] v1alpha1 . ApplicationSource { state . SyncResult . Source }
revisions = [ ] string { state . SyncResult . Revision }
2022-12-16 20:47:08 +00:00
}
2023-11-02 15:51:16 +00:00
// ignore error if CompareStateRepoError, this shouldn't happen as noRevisionCache is true
2025-07-16 17:39:30 +00:00
compareResult , err := m . CompareAppState ( app , project , revisions , sources , false , true , syncOp . Manifests , isMultiSourceSync )
2025-03-27 16:37:52 +00:00
if err != nil && ! stderrors . Is ( err , ErrCompareStateRepo ) {
2023-11-02 15:51:16 +00:00
state . Phase = common . OperationError
state . Message = err . Error ( )
return
}
2022-12-16 20:47:08 +00:00
2025-07-16 17:39:30 +00:00
// We are now guaranteed to have a concrete commit SHA. Save this in the sync result revision so that we remember
// what we should be syncing to when resuming operations.
state . SyncResult . Revision = compareResult . syncStatus . Revision
state . SyncResult . Revisions = compareResult . syncStatus . Revisions
2018-12-04 10:52:57 +00:00
2025-07-16 17:39:30 +00:00
// validates if it should fail the sync on that revision if it finds shared resources
2025-07-02 19:00:57 +00:00
hasSharedResource , sharedResourceMessage := hasSharedResourceCondition ( app )
2025-07-16 17:39:30 +00:00
if syncOp . SyncOptions . HasOption ( "FailOnSharedResource=true" ) && hasSharedResource {
2025-07-02 19:00:57 +00:00
state . Phase = common . OperationFailed
2025-07-16 17:39:30 +00:00
state . Message = "Shared resource found: " + sharedResourceMessage
2025-07-02 19:00:57 +00:00
return
}
2019-10-11 00:26:53 +00:00
// If there are any comparison or spec errors error conditions do not perform the operation
if errConditions := app . Status . GetConditions ( map [ v1alpha1 . ApplicationConditionType ] bool {
v1alpha1 . ApplicationConditionComparisonError : true ,
v1alpha1 . ApplicationConditionInvalidSpecError : true ,
} ) ; len ( errConditions ) > 0 {
2020-05-15 17:01:18 +00:00
state . Phase = common . OperationError
2018-07-10 21:45:18 +00:00
state . Message = argo . FormatAppConditions ( errConditions )
return
}
2018-12-04 10:52:57 +00:00
2025-01-13 18:15:42 +00:00
destCluster , err := argo . GetDestinationCluster ( context . Background ( ) , app . Spec . Destination , m . db )
2018-07-07 07:54:06 +00:00
if err != nil {
2020-05-15 17:01:18 +00:00
state . Phase = common . OperationError
2025-01-13 18:15:42 +00:00
state . Message = fmt . Sprintf ( "Failed to get destination cluster: %v" , err )
2018-07-07 07:54:06 +00:00
return
}
2025-01-13 18:15:42 +00:00
rawConfig , err := destCluster . RawRestConfig ( )
2024-10-06 14:55:26 +00:00
if err != nil {
state . Phase = common . OperationError
state . Message = err . Error ( )
return
}
2025-01-13 18:15:42 +00:00
clusterRESTConfig , err := destCluster . RESTConfig ( )
2024-10-06 14:55:26 +00:00
if err != nil {
state . Phase = common . OperationError
state . Message = err . Error ( )
return
}
restConfig := metrics . AddMetricsTransportWrapper ( m . metricsServer , app , clusterRESTConfig )
2019-07-24 22:00:40 +00:00
2019-06-21 22:59:05 +00:00
resourceOverrides , err := m . settingsMgr . GetResourceOverrides ( )
if err != nil {
2020-05-15 17:01:18 +00:00
state . Phase = common . OperationError
2019-06-21 22:59:05 +00:00
state . Message = fmt . Sprintf ( "Failed to load resource overrides: %v" , err )
return
}
2025-07-16 17:39:30 +00:00
initialResourcesRes := make ( [ ] common . ResourceSyncResult , len ( state . SyncResult . Resources ) )
for i , res := range state . SyncResult . Resources {
2020-05-15 17:01:18 +00:00
key := kube . ResourceKey { Group : res . Group , Kind : res . Kind , Namespace : res . Namespace , Name : res . Name }
2025-05-15 13:26:34 +00:00
initialResourcesRes [ i ] = common . ResourceSyncResult {
2020-05-15 17:01:18 +00:00
ResourceKey : key ,
Message : res . Message ,
Status : res . Status ,
HookPhase : res . HookPhase ,
HookType : res . HookType ,
SyncPhase : res . SyncPhase ,
Version : res . Version ,
2025-05-27 19:58:28 +00:00
Images : res . Images ,
2020-05-15 17:01:18 +00:00
Order : i + 1 ,
2025-05-15 13:26:34 +00:00
}
2020-05-15 17:01:18 +00:00
}
2021-01-29 20:26:47 +00:00
2024-12-31 08:34:11 +00:00
prunePropagationPolicy := metav1 . DeletePropagationForeground
2021-03-12 17:01:36 +00:00
switch {
case syncOp . SyncOptions . HasOption ( "PrunePropagationPolicy=background" ) :
2024-12-31 08:34:11 +00:00
prunePropagationPolicy = metav1 . DeletePropagationBackground
2021-03-12 17:01:36 +00:00
case syncOp . SyncOptions . HasOption ( "PrunePropagationPolicy=foreground" ) :
2024-12-31 08:34:11 +00:00
prunePropagationPolicy = metav1 . DeletePropagationForeground
2021-03-12 17:01:36 +00:00
case syncOp . SyncOptions . HasOption ( "PrunePropagationPolicy=orphan" ) :
2024-12-31 08:34:11 +00:00
prunePropagationPolicy = metav1 . DeletePropagationOrphan
2021-03-12 17:01:36 +00:00
}
2025-06-13 21:58:07 +00:00
clientSideApplyManager := common . DefaultClientSideApplyMigrationManager
// Check for custom field manager from application annotation
if managerValue := app . GetAnnotation ( cdcommon . AnnotationClientSideApplyMigrationManager ) ; managerValue != "" {
clientSideApplyManager = managerValue
}
2025-01-13 18:15:42 +00:00
openAPISchema , err := m . getOpenAPISchema ( destCluster )
2021-06-17 19:06:18 +00:00
if err != nil {
state . Phase = common . OperationError
state . Message = fmt . Sprintf ( "failed to load openAPISchema: %v" , err )
return
}
2022-01-13 21:00:17 +00:00
reconciliationResult := compareResult . reconciliationResult
// if RespectIgnoreDifferences is enabled, it should normalize the target
// resources which in this case applies the live values in the configured
// ignore differences fields.
if syncOp . SyncOptions . HasOption ( "RespectIgnoreDifferences=true" ) {
2025-11-17 19:21:44 +00:00
patchedTargets , err := normalizeTargetResources ( compareResult )
2022-01-13 21:00:17 +00:00
if err != nil {
state . Phase = common . OperationError
state . Message = fmt . Sprintf ( "Failed to normalize target resources: %s" , err )
return
}
reconciliationResult . Target = patchedTargets
}
2024-10-05 00:54:37 +00:00
installationID , err := m . settingsMgr . GetInstallationID ( )
if err != nil {
log . Errorf ( "Could not get installation ID: %v" , err )
return
}
2025-05-15 08:26:47 +00:00
trackingMethod , err := m . settingsMgr . GetTrackingMethod ( )
if err != nil {
log . Errorf ( "Could not get trackingMethod: %v" , err )
return
}
2022-08-08 17:37:45 +00:00
2024-10-03 14:37:50 +00:00
impersonationEnabled , err := m . settingsMgr . IsImpersonationEnabled ( )
if err != nil {
log . Errorf ( "could not get impersonation feature flag: %v" , err )
return
}
if impersonationEnabled {
2025-07-03 17:16:58 +00:00
serviceAccountToImpersonate , err := deriveServiceAccountToImpersonate ( project , app , destCluster )
2024-09-04 18:18:47 +00:00
if err != nil {
state . Phase = common . OperationError
state . Message = fmt . Sprintf ( "failed to find a matching service account to impersonate: %v" , err )
return
}
logEntry = logEntry . WithFields ( log . Fields { "impersonationEnabled" : "true" , "serviceAccount" : serviceAccountToImpersonate } )
// set the impersonation headers.
rawConfig . Impersonate = rest . ImpersonationConfig {
UserName : serviceAccountToImpersonate ,
}
restConfig . Impersonate = rest . ImpersonationConfig {
UserName : serviceAccountToImpersonate ,
}
}
2022-11-04 12:59:16 +00:00
opts := [ ] sync . SyncOpt {
2020-10-27 21:10:24 +00:00
sync . WithLogr ( logutils . NewLogrusLogger ( logEntry ) ) ,
2020-05-15 17:01:18 +00:00
sync . WithHealthOverride ( lua . ResourceHealthOverrides ( resourceOverrides ) ) ,
2024-12-31 08:34:11 +00:00
sync . WithPermissionValidator ( func ( un * unstructured . Unstructured , res * metav1 . APIResource ) error {
2025-12-03 20:55:28 +00:00
if ! project . IsGroupKindNamePermitted ( un . GroupVersionKind ( ) . GroupKind ( ) , un . GetName ( ) , res . Namespaced ) {
2025-07-03 17:16:58 +00:00
return fmt . Errorf ( "resource %s:%s is not permitted in project %s" , un . GroupVersionKind ( ) . Group , un . GroupVersionKind ( ) . Kind , project . Name )
2019-06-05 01:17:41 +00:00
}
2022-09-08 11:33:10 +00:00
if res . Namespaced {
2025-07-03 17:16:58 +00:00
permitted , err := project . IsDestinationPermitted ( destCluster , un . GetNamespace ( ) , func ( project string ) ( [ ] * v1alpha1 . Cluster , error ) {
2022-09-08 11:33:10 +00:00
return m . db . GetProjectClusters ( context . TODO ( ) , project )
} )
if err != nil {
return err
}
if ! permitted {
2025-07-03 17:16:58 +00:00
return fmt . Errorf ( "namespace %v is not permitted in project '%s'" , un . GetNamespace ( ) , project . Name )
2022-09-08 11:33:10 +00:00
}
2018-07-12 02:12:30 +00:00
}
2020-05-15 17:01:18 +00:00
return nil
} ) ,
sync . WithOperationSettings ( syncOp . DryRun , syncOp . Prune , syncOp . SyncStrategy . Force ( ) , syncOp . IsApplyStrategy ( ) || len ( syncOp . Resources ) > 0 ) ,
2020-11-13 09:18:15 +00:00
sync . WithInitialState ( state . Phase , state . Message , initialResourcesRes , state . StartedAt ) ,
2020-05-15 17:01:18 +00:00
sync . WithResourcesFilter ( func ( key kube . ResourceKey , target * unstructured . Unstructured , live * unstructured . Unstructured ) bool {
2022-08-08 17:37:45 +00:00
return ( len ( syncOp . Resources ) == 0 ||
2023-12-18 16:40:23 +00:00
isPostDeleteHook ( target ) ||
2025-12-05 20:27:03 +00:00
isPreDeleteHook ( target ) ||
2022-08-08 17:37:45 +00:00
argo . ContainsSyncResource ( key . Name , key . Namespace , schema . GroupVersionKind { Kind : key . Kind , Group : key . Group } , syncOp . Resources ) ) &&
2025-05-15 08:26:47 +00:00
m . isSelfReferencedObj ( live , target , app . GetName ( ) , v1alpha1 . TrackingMethod ( trackingMethod ) , installationID )
2020-05-15 17:01:18 +00:00
} ) ,
2021-03-18 05:02:43 +00:00
sync . WithManifestValidation ( ! syncOp . SyncOptions . HasOption ( common . SyncOptionsDisableValidation ) ) ,
2020-11-02 20:17:43 +00:00
sync . WithSyncWaveHook ( delayBetweenSyncWaves ) ,
2021-03-18 05:02:43 +00:00
sync . WithPruneLast ( syncOp . SyncOptions . HasOption ( common . SyncOptionPruneLast ) ) ,
2021-01-29 20:26:47 +00:00
sync . WithResourceModificationChecker ( syncOp . SyncOptions . HasOption ( "ApplyOutOfSyncOnly=true" ) , compareResult . diffResultList ) ,
2021-03-12 17:01:36 +00:00
sync . WithPrunePropagationPolicy ( & prunePropagationPolicy ) ,
2021-03-18 05:02:43 +00:00
sync . WithReplace ( syncOp . SyncOptions . HasOption ( common . SyncOptionReplace ) ) ,
2022-08-05 23:16:35 +00:00
sync . WithServerSideApply ( syncOp . SyncOptions . HasOption ( common . SyncOptionServerSideApply ) ) ,
sync . WithServerSideApplyManager ( cdcommon . ArgoCDSSAManager ) ,
2025-06-13 21:58:07 +00:00
sync . WithClientSideApplyMigration (
! syncOp . SyncOptions . HasOption ( common . SyncOptionDisableClientSideApplyMigration ) ,
clientSideApplyManager ,
) ,
2024-10-24 07:08:24 +00:00
sync . WithPruneConfirmed ( app . IsDeletionConfirmed ( state . StartedAt . Time ) ) ,
2025-04-20 11:54:38 +00:00
sync . WithSkipDryRunOnMissingResource ( syncOp . SyncOptions . HasOption ( common . SyncOptionSkipDryRunOnMissingResource ) ) ,
2022-11-04 12:59:16 +00:00
}
if syncOp . SyncOptions . HasOption ( "CreateNamespace=true" ) {
2024-08-15 11:29:51 +00:00
opts = append ( opts , sync . WithNamespaceModifier ( syncNamespace ( app . Spec . SyncPolicy ) ) )
2022-11-04 12:59:16 +00:00
}
syncCtx , cleanup , err := sync . NewSyncContext (
compareResult . syncStatus . Revision ,
reconciliationResult ,
restConfig ,
rawConfig ,
m . kubectl ,
app . Spec . Destination . Namespace ,
openAPISchema ,
opts ... ,
2020-05-15 17:01:18 +00:00
)
2018-07-07 07:54:06 +00:00
if err != nil {
2020-05-15 17:01:18 +00:00
state . Phase = common . OperationError
2021-05-28 14:25:35 +00:00
state . Message = fmt . Sprintf ( "failed to initialize sync context: %v" , err )
return
2019-07-24 22:00:40 +00:00
}
2018-07-07 07:54:06 +00:00
2021-05-27 17:20:53 +00:00
defer cleanup ( )
2020-05-15 17:01:18 +00:00
start := time . Now ( )
2018-09-20 16:48:54 +00:00
2020-05-15 17:01:18 +00:00
if state . Phase == common . OperationTerminating {
syncCtx . Terminate ( )
2019-06-05 01:17:41 +00:00
} else {
2020-05-15 17:01:18 +00:00
syncCtx . Sync ( )
}
var resState [ ] common . ResourceSyncResult
state . Phase , state . Message , resState = syncCtx . GetState ( )
state . SyncResult . Resources = nil
2023-04-03 15:48:50 +00:00
2023-05-19 13:55:08 +00:00
if app . Spec . SyncPolicy != nil {
state . SyncResult . ManagedNamespaceMetadata = app . Spec . SyncPolicy . ManagedNamespaceMetadata
}
2023-04-03 15:48:50 +00:00
var apiVersion [ ] kube . APIResourceInfo
2020-05-15 17:01:18 +00:00
for _ , res := range resState {
2023-04-03 15:48:50 +00:00
augmentedMsg , err := argo . AugmentSyncMsg ( res , func ( ) ( [ ] kube . APIResourceInfo , error ) {
if apiVersion == nil {
2025-01-13 18:15:42 +00:00
_ , apiVersion , err = m . liveStateCache . GetVersionsInfo ( destCluster )
2023-04-03 15:48:50 +00:00
if err != nil {
2025-01-13 18:15:42 +00:00
return nil , fmt . Errorf ( "failed to get version info from the target cluster %q" , destCluster . Server )
2023-04-03 15:48:50 +00:00
}
}
return apiVersion , nil
} )
if err != nil {
log . Errorf ( "using the original message since: %v" , err )
} else {
res . Message = augmentedMsg
}
2020-05-15 17:01:18 +00:00
state . SyncResult . Resources = append ( state . SyncResult . Resources , & v1alpha1 . ResourceResult {
HookType : res . HookType ,
Group : res . ResourceKey . Group ,
Kind : res . ResourceKey . Kind ,
Namespace : res . ResourceKey . Namespace ,
Name : res . ResourceKey . Name ,
Version : res . Version ,
SyncPhase : res . SyncPhase ,
HookPhase : res . HookPhase ,
Status : res . Status ,
Message : res . Message ,
2025-05-27 19:58:28 +00:00
Images : res . Images ,
2020-05-15 17:01:18 +00:00
} )
}
logEntry . WithField ( "duration" , time . Since ( start ) ) . Info ( "sync/terminate complete" )
if ! syncOp . DryRun && len ( syncOp . Resources ) == 0 && state . Phase . Successful ( ) {
2025-07-16 17:39:30 +00:00
err := m . persistRevisionHistory ( app , compareResult . syncStatus . Revision , compareResult . syncStatus . ComparedTo . Source , compareResult . syncStatus . Revisions , compareResult . syncStatus . ComparedTo . Sources , isMultiSourceSync , state . StartedAt , state . Operation . InitiatedBy )
2020-05-15 17:01:18 +00:00
if err != nil {
state . Phase = common . OperationError
state . Message = fmt . Sprintf ( "failed to record sync to history: %v" , err )
2019-06-05 01:17:41 +00:00
}
2019-01-18 15:32:50 +00:00
}
2018-09-10 17:14:14 +00:00
}
2020-11-02 20:17:43 +00:00
2024-04-03 22:56:59 +00:00
// normalizeTargetResources modifies target resources to ensure ignored fields are not touched during synchronization:
// - applies normalization to the target resources based on the live resources
// - copies ignored fields from the matching live resources: apply normalizer to the live resource,
// calculates the patch performed by normalizer and applies the patch to the target resource
2025-11-17 19:21:44 +00:00
func normalizeTargetResources ( cr * comparisonResult ) ( [ ] * unstructured . Unstructured , error ) {
// normalize live and target resources
2022-01-13 21:00:17 +00:00
normalized , err := diff . Normalize ( cr . reconciliationResult . Live , cr . reconciliationResult . Target , cr . diffConfig )
if err != nil {
return nil , err
}
patchedTargets := [ ] * unstructured . Unstructured { }
for idx , live := range cr . reconciliationResult . Live {
normalizedTarget := normalized . Targets [ idx ]
if normalizedTarget == nil {
patchedTargets = append ( patchedTargets , nil )
continue
}
2022-01-19 22:57:43 +00:00
originalTarget := cr . reconciliationResult . Target [ idx ]
if live == nil {
patchedTargets = append ( patchedTargets , originalTarget )
continue
}
2022-01-13 21:00:17 +00:00
2025-11-17 19:21:44 +00:00
var lookupPatchMeta * strategicpatch . PatchMetaFromStruct
versionedObject , err := scheme . Scheme . New ( normalizedTarget . GroupVersionKind ( ) )
if err == nil {
meta , err := strategicpatch . NewPatchMetaFromStruct ( versionedObject )
if err != nil {
2022-01-13 21:00:17 +00:00
return nil , err
}
2025-11-17 19:21:44 +00:00
lookupPatchMeta = & meta
2022-01-13 21:00:17 +00:00
}
2024-04-03 22:56:59 +00:00
livePatch , err := getMergePatch ( normalized . Lives [ idx ] , live , lookupPatchMeta )
if err != nil {
return nil , err
}
2022-01-13 21:00:17 +00:00
2025-11-17 19:21:44 +00:00
normalizedTarget , err = applyMergePatch ( normalizedTarget , livePatch , versionedObject )
2024-04-03 22:56:59 +00:00
if err != nil {
return nil , err
2022-01-13 21:00:17 +00:00
}
2025-11-17 19:21:44 +00:00
2024-04-03 22:56:59 +00:00
patchedTargets = append ( patchedTargets , normalizedTarget )
2022-01-13 21:00:17 +00:00
}
2024-04-03 22:56:59 +00:00
return patchedTargets , nil
2022-01-13 21:00:17 +00:00
}
// getMergePatch calculates and returns the patch between the original and the
// modified unstructures.
2025-11-17 19:21:44 +00:00
func getMergePatch ( original , modified * unstructured . Unstructured , lookupPatchMeta * strategicpatch . PatchMetaFromStruct ) ( [ ] byte , error ) {
2022-01-13 21:00:17 +00:00
originalJSON , err := original . MarshalJSON ( )
if err != nil {
return nil , err
}
modifiedJSON , err := modified . MarshalJSON ( )
if err != nil {
return nil , err
}
2024-04-03 22:56:59 +00:00
if lookupPatchMeta != nil {
return strategicpatch . CreateThreeWayMergePatch ( modifiedJSON , modifiedJSON , originalJSON , lookupPatchMeta , true )
}
2022-01-13 21:00:17 +00:00
return jsonpatch . CreateMergePatch ( originalJSON , modifiedJSON )
}
2025-11-17 19:21:44 +00:00
// applyMergePatch will apply the given patch in the obj and return the patched
// unstructure.
func applyMergePatch ( obj * unstructured . Unstructured , patch [ ] byte , versionedObject any ) ( * unstructured . Unstructured , error ) {
2022-01-13 21:00:17 +00:00
originalJSON , err := obj . MarshalJSON ( )
if err != nil {
return nil , err
}
2024-04-03 22:56:59 +00:00
var patchedJSON [ ] byte
2025-11-17 19:21:44 +00:00
if versionedObject == nil {
2025-09-15 14:28:13 +00:00
patchedJSON , err = jsonpatch . MergePatch ( originalJSON , patch )
2025-11-17 19:21:44 +00:00
} else {
patchedJSON , err = strategicpatch . StrategicMergePatch ( originalJSON , patch , versionedObject )
2024-04-03 22:56:59 +00:00
}
2022-01-13 21:00:17 +00:00
if err != nil {
return nil , err
}
2024-04-03 22:56:59 +00:00
2022-01-13 21:00:17 +00:00
patchedObj := & unstructured . Unstructured { }
_ , _ , err = unstructured . UnstructuredJSONScheme . Decode ( patchedJSON , nil , patchedObj )
if err != nil {
return nil , err
}
return patchedObj , nil
}
2021-10-20 15:06:41 +00:00
// hasSharedResourceCondition will check if the Application has any resource that has already
// been synced by another Application. If the resource is found in another Application it returns
// true along with a human readable message of which specific resource has this condition.
func hasSharedResourceCondition ( app * v1alpha1 . Application ) ( bool , string ) {
for _ , condition := range app . Status . Conditions {
if condition . Type == v1alpha1 . ApplicationConditionSharedResourceWarning {
return true , condition . Message
}
}
return false , ""
}
2020-11-02 20:17:43 +00:00
// delayBetweenSyncWaves is a gitops-engine SyncWaveHook which introduces an artificial delay
// between each sync wave. We introduce an artificial delay in order give other controllers a
// _chance_ to react to the spec change that we just applied. This is important because without
// this, Argo CD will likely assess resource health too quickly (against the stale object), causing
// hooks to fire prematurely. See: https://github.com/argoproj/argo-cd/issues/4669.
// Note, this is not foolproof, since a proper fix would require the CRD record
// status.observedGeneration coupled with a health.lua that verifies
// status.observedGeneration == metadata.generation
2025-01-07 15:12:56 +00:00
func delayBetweenSyncWaves ( _ common . SyncPhase , _ int , finalWave bool ) error {
2020-11-02 20:17:43 +00:00
if ! finalWave {
delaySec := 2
if delaySecStr := os . Getenv ( EnvVarSyncWaveDelay ) ; delaySecStr != "" {
if val , err := strconv . Atoi ( delaySecStr ) ; err == nil {
delaySec = val
}
}
duration := time . Duration ( delaySec ) * time . Second
time . Sleep ( duration )
}
return nil
}
2024-03-26 12:45:35 +00:00
2024-10-12 17:32:46 +00:00
func syncWindowPreventsSync ( app * v1alpha1 . Application , proj * v1alpha1 . AppProject ) ( bool , error ) {
2024-03-26 12:45:35 +00:00
window := proj . Spec . SyncWindows . Matches ( app )
isManual := false
if app . Status . OperationState != nil {
isManual = ! app . Status . OperationState . Operation . InitiatedBy . Automated
}
2024-10-12 17:32:46 +00:00
canSync , err := window . CanSync ( isManual )
if err != nil {
// prevents sync because sync window has an error
return true , err
}
return ! canSync , nil
2024-03-26 12:45:35 +00:00
}
2024-09-04 18:18:47 +00:00
2024-10-03 14:37:50 +00:00
// deriveServiceAccountToImpersonate determines the service account to be used for impersonation for the sync operation.
2024-09-04 18:18:47 +00:00
// The returned service account will be fully qualified including namespace and the service account name in the format system:serviceaccount:<namespace>:<service_account>
2025-06-23 01:32:19 +00:00
func deriveServiceAccountToImpersonate ( project * v1alpha1 . AppProject , application * v1alpha1 . Application , destCluster * v1alpha1 . Cluster ) ( string , error ) {
2024-09-04 18:18:47 +00:00
// spec.Destination.Namespace is optional. If not specified, use the Application's
// namespace
serviceAccountNamespace := application . Spec . Destination . Namespace
if serviceAccountNamespace == "" {
serviceAccountNamespace = application . Namespace
}
// Loop through the destinationServiceAccounts and see if there is any destination that is a candidate.
// if so, return the service account specified for that destination.
for _ , item := range project . Spec . DestinationServiceAccounts {
2025-06-23 01:32:19 +00:00
dstServerMatched , err := glob . MatchWithError ( item . Server , destCluster . Server )
2024-10-03 14:37:50 +00:00
if err != nil {
return "" , fmt . Errorf ( "invalid glob pattern for destination server: %w" , err )
}
dstNamespaceMatched , err := glob . MatchWithError ( item . Namespace , application . Spec . Destination . Namespace )
if err != nil {
return "" , fmt . Errorf ( "invalid glob pattern for destination namespace: %w" , err )
}
2024-09-04 18:18:47 +00:00
if dstServerMatched && dstNamespaceMatched {
2024-10-03 14:37:50 +00:00
if strings . Trim ( item . DefaultServiceAccount , " " ) == "" || strings . ContainsAny ( item . DefaultServiceAccount , serviceAccountDisallowedCharSet ) {
return "" , fmt . Errorf ( "default service account contains invalid chars '%s'" , item . DefaultServiceAccount )
} else if strings . Contains ( item . DefaultServiceAccount , ":" ) {
2024-09-04 18:18:47 +00:00
// service account is specified along with its namespace.
2024-12-20 16:22:28 +00:00
return "system:serviceaccount:" + item . DefaultServiceAccount , nil
2024-09-04 18:18:47 +00:00
}
2025-01-07 15:25:22 +00:00
// service account needs to be prefixed with a namespace
return fmt . Sprintf ( "system:serviceaccount:%s:%s" , serviceAccountNamespace , item . DefaultServiceAccount ) , nil
2024-09-04 18:18:47 +00:00
}
}
// if there is no match found in the AppProject.Spec.DestinationServiceAccounts, use the default service account of the destination namespace.
return "" , fmt . Errorf ( "no matching service account found for destination server %s and namespace %s" , application . Spec . Destination . Server , serviceAccountNamespace )
}