github.com/argoproj/argo-cd@v1.8.7/controller/sync.go (about) 1 package controller 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strconv" 8 "sync/atomic" 9 "time" 10 11 "github.com/argoproj/gitops-engine/pkg/sync" 12 "github.com/argoproj/gitops-engine/pkg/sync/common" 13 "github.com/argoproj/gitops-engine/pkg/utils/kube" 14 log "github.com/sirupsen/logrus" 15 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 "k8s.io/apimachinery/pkg/runtime/schema" 18 19 cdcommon "github.com/argoproj/argo-cd/common" 20 "github.com/argoproj/argo-cd/controller/metrics" 21 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 22 listersv1alpha1 "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1" 23 "github.com/argoproj/argo-cd/util/argo" 24 logutils "github.com/argoproj/argo-cd/util/log" 25 "github.com/argoproj/argo-cd/util/lua" 26 "github.com/argoproj/argo-cd/util/rand" 27 ) 28 29 var syncIdPrefix uint64 = 0 30 31 const ( 32 // EnvVarSyncWaveDelay is an environment variable which controls the delay in seconds between 33 // each sync-wave 34 EnvVarSyncWaveDelay = "ARGOCD_SYNC_WAVE_DELAY" 35 ) 36 37 func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState) { 38 // Sync requests might be requested with ambiguous revisions (e.g. master, HEAD, v1.2.3). 39 // This can change meaning when resuming operations (e.g a hook sync). After calculating a 40 // concrete git commit SHA, the SHA is remembered in the status.operationState.syncResult field. 41 // This ensures that when resuming an operation, we sync to the same revision that we initially 42 // started with. 43 var revision string 44 var syncOp v1alpha1.SyncOperation 45 var syncRes *v1alpha1.SyncOperationResult 46 var source v1alpha1.ApplicationSource 47 48 if state.Operation.Sync == nil { 49 state.Phase = common.OperationFailed 50 state.Message = "Invalid operation request: no operation specified" 51 return 52 } 53 syncOp = *state.Operation.Sync 54 if syncOp.Source == nil { 55 // normal sync case (where source is taken from app.spec.source) 56 source = app.Spec.Source 57 } else { 58 // rollback case 59 source = *state.Operation.Sync.Source 60 } 61 62 if state.SyncResult != nil { 63 syncRes = state.SyncResult 64 revision = state.SyncResult.Revision 65 } else { 66 syncRes = &v1alpha1.SyncOperationResult{} 67 // status.operationState.syncResult.source. must be set properly since auto-sync relies 68 // on this information to decide if it should sync (if source is different than the last 69 // sync attempt) 70 syncRes.Source = source 71 state.SyncResult = syncRes 72 } 73 74 if revision == "" { 75 // if we get here, it means we did not remember a commit SHA which we should be syncing to. 76 // This typically indicates we are just about to begin a brand new sync/rollback operation. 77 // Take the value in the requested operation. We will resolve this to a SHA later. 78 revision = syncOp.Revision 79 } 80 81 proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace, m.settingsMgr) 82 if err != nil { 83 state.Phase = common.OperationError 84 state.Message = fmt.Sprintf("Failed to load application project: %v", err) 85 return 86 } 87 88 compareResult := m.CompareAppState(app, proj, revision, source, false, syncOp.Manifests) 89 // We now have a concrete commit SHA. Save this in the sync result revision so that we remember 90 // what we should be syncing to when resuming operations. 91 syncRes.Revision = compareResult.syncStatus.Revision 92 93 // If there are any comparison or spec errors error conditions do not perform the operation 94 if errConditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{ 95 v1alpha1.ApplicationConditionComparisonError: true, 96 v1alpha1.ApplicationConditionInvalidSpecError: true, 97 }); len(errConditions) > 0 { 98 state.Phase = common.OperationError 99 state.Message = argo.FormatAppConditions(errConditions) 100 return 101 } 102 103 clst, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server) 104 if err != nil { 105 state.Phase = common.OperationError 106 state.Message = err.Error() 107 return 108 } 109 110 rawConfig := clst.RawRestConfig() 111 restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig()) 112 113 resourceOverrides, err := m.settingsMgr.GetResourceOverrides() 114 if err != nil { 115 state.Phase = common.OperationError 116 state.Message = fmt.Sprintf("Failed to load resource overrides: %v", err) 117 return 118 } 119 120 atomic.AddUint64(&syncIdPrefix, 1) 121 syncId := fmt.Sprintf("%05d-%s", syncIdPrefix, rand.RandString(5)) 122 123 logEntry := log.WithFields(log.Fields{"application": app.Name, "syncId": syncId}) 124 initialResourcesRes := make([]common.ResourceSyncResult, 0) 125 for i, res := range syncRes.Resources { 126 key := kube.ResourceKey{Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name} 127 initialResourcesRes = append(initialResourcesRes, common.ResourceSyncResult{ 128 ResourceKey: key, 129 Message: res.Message, 130 Status: res.Status, 131 HookPhase: res.HookPhase, 132 HookType: res.HookType, 133 SyncPhase: res.SyncPhase, 134 Version: res.Version, 135 Order: i + 1, 136 }) 137 } 138 syncCtx, err := sync.NewSyncContext( 139 compareResult.syncStatus.Revision, 140 compareResult.reconciliationResult, 141 restConfig, 142 rawConfig, 143 m.kubectl, 144 app.Spec.Destination.Namespace, 145 sync.WithLogr(logutils.NewLogrusLogger(logEntry)), 146 sync.WithHealthOverride(lua.ResourceHealthOverrides(resourceOverrides)), 147 sync.WithPermissionValidator(func(un *unstructured.Unstructured, res *v1.APIResource) error { 148 if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), res.Namespaced) { 149 return fmt.Errorf("Resource %s:%s is not permitted in project %s.", un.GroupVersionKind().Group, un.GroupVersionKind().Kind, proj.Name) 150 } 151 if res.Namespaced && !proj.IsDestinationPermitted(v1alpha1.ApplicationDestination{Namespace: un.GetNamespace(), Server: app.Spec.Destination.Server}) { 152 return fmt.Errorf("namespace %v is not permitted in project '%s'", un.GetNamespace(), proj.Name) 153 } 154 return nil 155 }), 156 sync.WithOperationSettings(syncOp.DryRun, syncOp.Prune, syncOp.SyncStrategy.Force(), syncOp.IsApplyStrategy() || len(syncOp.Resources) > 0), 157 sync.WithInitialState(state.Phase, state.Message, initialResourcesRes, state.StartedAt), 158 sync.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool { 159 return len(syncOp.Resources) == 0 || argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources) 160 }), 161 sync.WithManifestValidation(!syncOp.SyncOptions.HasOption("Validate=false")), 162 sync.WithNamespaceCreation(syncOp.SyncOptions.HasOption("CreateNamespace=true"), func(un *unstructured.Unstructured) bool { 163 if un != nil && kube.GetAppInstanceLabel(un, cdcommon.LabelKeyAppInstance) != "" { 164 kube.UnsetLabel(un, cdcommon.LabelKeyAppInstance) 165 return true 166 } 167 return false 168 }), 169 sync.WithSyncWaveHook(delayBetweenSyncWaves), 170 ) 171 172 if err != nil { 173 state.Phase = common.OperationError 174 state.Message = fmt.Sprintf("failed to record sync to history: %v", err) 175 } 176 177 start := time.Now() 178 179 if state.Phase == common.OperationTerminating { 180 syncCtx.Terminate() 181 } else { 182 syncCtx.Sync() 183 } 184 var resState []common.ResourceSyncResult 185 state.Phase, state.Message, resState = syncCtx.GetState() 186 state.SyncResult.Resources = nil 187 for _, res := range resState { 188 state.SyncResult.Resources = append(state.SyncResult.Resources, &v1alpha1.ResourceResult{ 189 HookType: res.HookType, 190 Group: res.ResourceKey.Group, 191 Kind: res.ResourceKey.Kind, 192 Namespace: res.ResourceKey.Namespace, 193 Name: res.ResourceKey.Name, 194 Version: res.Version, 195 SyncPhase: res.SyncPhase, 196 HookPhase: res.HookPhase, 197 Status: res.Status, 198 Message: res.Message, 199 }) 200 } 201 202 logEntry.WithField("duration", time.Since(start)).Info("sync/terminate complete") 203 204 if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() { 205 err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source, state.StartedAt) 206 if err != nil { 207 state.Phase = common.OperationError 208 state.Message = fmt.Sprintf("failed to record sync to history: %v", err) 209 } 210 } 211 } 212 213 // delayBetweenSyncWaves is a gitops-engine SyncWaveHook which introduces an artificial delay 214 // between each sync wave. We introduce an artificial delay in order give other controllers a 215 // _chance_ to react to the spec change that we just applied. This is important because without 216 // this, Argo CD will likely assess resource health too quickly (against the stale object), causing 217 // hooks to fire prematurely. See: https://github.com/argoproj/argo-cd/issues/4669. 218 // Note, this is not foolproof, since a proper fix would require the CRD record 219 // status.observedGeneration coupled with a health.lua that verifies 220 // status.observedGeneration == metadata.generation 221 func delayBetweenSyncWaves(phase common.SyncPhase, wave int, finalWave bool) error { 222 if !finalWave { 223 delaySec := 2 224 if delaySecStr := os.Getenv(EnvVarSyncWaveDelay); delaySecStr != "" { 225 if val, err := strconv.Atoi(delaySecStr); err == nil { 226 delaySec = val 227 } 228 } 229 duration := time.Duration(delaySec) * time.Second 230 time.Sleep(duration) 231 } 232 return nil 233 }