diff --git a/controller/sync.go b/controller/sync.go index 34c12bdb5da3c..401d08bc56ea4 100644 --- a/controller/sync.go +++ b/controller/sync.go @@ -161,6 +161,12 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha state.Phase = common.OperationError state.Message = fmt.Sprintf("Failed to load application project: %v", err) return + } else if syncWindowPreventsSync(app, proj) { + // 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" + } + return } if app.Spec.HasMultipleSources() { @@ -573,3 +579,12 @@ func delayBetweenSyncWaves(phase common.SyncPhase, wave int, finalWave bool) err } return nil } + +func syncWindowPreventsSync(app *v1alpha1.Application, proj *v1alpha1.AppProject) bool { + window := proj.Spec.SyncWindows.Matches(app) + isManual := false + if app.Status.OperationState != nil { + isManual = !app.Status.OperationState.Operation.InitiatedBy.Automated + } + return !window.CanSync(isManual) +} diff --git a/controller/sync_test.go b/controller/sync_test.go index 309f846ca6460..f9bd81c1c138a 100644 --- a/controller/sync_test.go +++ b/controller/sync_test.go @@ -254,6 +254,75 @@ func TestAppStateManager_SyncAppState(t *testing.T) { }) } +func TestSyncWindowDeniesSync(t *testing.T) { + type fixture struct { + project *v1alpha1.AppProject + application *v1alpha1.Application + controller *ApplicationController + } + + setup := func() *fixture { + app := newFakeApp() + app.Status.OperationState = nil + app.Status.History = nil + + project := &v1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Namespace: test.FakeArgoCDNamespace, + Name: "default", + }, + Spec: v1alpha1.AppProjectSpec{ + SyncWindows: v1alpha1.SyncWindows{{ + Kind: "deny", + Schedule: "0 0 * * *", + Duration: "24h", + Clusters: []string{"*"}, + Namespaces: []string{"*"}, + Applications: []string{"*"}, + }}, + }, + } + data := fakeData{ + apps: []runtime.Object{app, project}, + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured), + } + ctrl := newFakeController(&data, nil) + + return &fixture{ + project: project, + application: app, + controller: ctrl, + } + } + + t.Run("will keep the sync progressing if a sync window prevents the sync", func(t *testing.T) { + // given a project with an active deny sync window and an operation in progress + t.Parallel() + f := setup() + opMessage := "Sync operation blocked by sync window" + + opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{ + Sync: &v1alpha1.SyncOperation{ + Source: &v1alpha1.ApplicationSource{}, + }}, + Phase: common.OperationRunning, + } + // when + f.controller.appStateManager.SyncAppState(f.application, opState) + + //then + assert.Equal(t, common.OperationRunning, opState.Phase) + assert.Contains(t, opState.Message, opMessage) + }) + +} + func TestNormalizeTargetResources(t *testing.T) { type fixture struct { comparisonResult *comparisonResult