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  }