feat: add sync overrun option to sync windows (#25361) (#25510)
Some checks are pending
Integration tests / Run end-to-end tests (push) Blocked by required conditions
Integration tests / E2E Tests - Composite result (push) Blocked by required conditions
Integration tests / changes (push) Waiting to run
Integration tests / Ensure Go modules synchronicity (push) Blocked by required conditions
Integration tests / Build & cache Go code (push) Blocked by required conditions
Integration tests / Lint Go code (push) Blocked by required conditions
Integration tests / Run unit tests for Go packages (push) Blocked by required conditions
Integration tests / Run unit tests with -race for Go packages (push) Blocked by required conditions
Integration tests / Check changes to generated code (push) Blocked by required conditions
Integration tests / Build, test & lint UI code (push) Blocked by required conditions
Integration tests / shellcheck (push) Waiting to run
Integration tests / Process & analyze test artifacts (push) Blocked by required conditions
Code scanning - action / CodeQL-Build (push) Waiting to run
Image / set-vars (push) Waiting to run
Image / build-only (push) Blocked by required conditions
Image / build-and-publish (push) Blocked by required conditions
Image / build-and-publish-provenance (push) Blocked by required conditions
Image / Deploy (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

Signed-off-by: Vilius Puškunalis <47086537+puskunalis@users.noreply.github.com>
This commit is contained in:
Vilius Puskunalis 2026-04-20 09:55:13 +03:00 committed by GitHub
parent 9c8ae9a294
commit 611fcb012c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2418 additions and 915 deletions

4
assets/swagger.json generated
View file

@ -10935,6 +10935,10 @@
"type": "string",
"title": "Schedule is the time the window will begin, specified in cron format"
},
"syncOverrun": {
"type": "boolean",
"title": "SyncOverrun allows ongoing syncs to continue in two scenarios:\nFor deny windows: allows syncs that started before the deny window became active to continue running\nFor allow windows: allows syncs that started during the allow window to continue after the window ends"
},
"timeZone": {
"type": "string",
"title": "TimeZone of the sync that will be applied to the schedule"

View file

@ -693,7 +693,7 @@ func printAppSummaryTable(app *argoappv1.Application, appURL string, windows *ar
}
if deny || !deny && !allow && inactiveAllows {
s, err := windows.CanSync(true)
s, err := windows.CanSync(true, nil)
if err == nil && s {
status = "Manual Allowed"
} else {

View file

@ -42,6 +42,8 @@ argocd proj windows list <project-name>`,
}
roleCommand.AddCommand(NewProjectWindowsDisableManualSyncCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsEnableManualSyncCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsDisableSyncOverrunCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsEnableSyncOverrunCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsAddWindowCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsDeleteCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsListCommand(clientOpts))
@ -49,18 +51,13 @@ argocd proj windows list <project-name>`,
return roleCommand
}
// NewProjectWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "disable-manual-sync PROJECT ID",
Short: "Disable manual sync for a sync window",
Long: "Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Example: `
#Disable manual sync for a sync window for the Project
argocd proj windows disable-manual-sync PROJECT ID
#Disabling manual sync for a windows set on the default project with Id 0
argocd proj windows disable-manual-sync default 0`,
// newProjectWindowsToggleCommand creates a command for toggling a boolean field on a sync window
func newProjectWindowsToggleCommand(clientOpts *argocdclient.ClientOptions, use, short, long, example string, updateFn func(*v1alpha1.SyncWindow)) *cobra.Command {
return &cobra.Command{
Use: use,
Short: short,
Long: long,
Example: example,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
@ -79,26 +76,51 @@ argocd proj windows disable-manual-sync default 0`,
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
found := false
for i, window := range proj.Spec.SyncWindows {
if id == i {
window.ManualSync = false
updateFn(window)
found = true
break
}
}
if !found {
errors.CheckError(fmt.Errorf("window with id '%d' not found", id))
}
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
// NewProjectWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"disable-manual-sync PROJECT ID",
"Disable manual sync for a sync window",
"Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Disable manual sync for a sync window for the Project
argocd proj windows disable-manual-sync PROJECT ID
#Disabling manual sync for a windows set on the default project with Id 0
argocd proj windows disable-manual-sync default 0`,
func(window *v1alpha1.SyncWindow) {
window.ManualSync = false
},
)
}
// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `argocd proj windows enable-manual-sync` command
func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "enable-manual-sync PROJECT ID",
Short: "Enable manual sync for a sync window",
Long: "Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Example: `
return newProjectWindowsToggleCommand(
clientOpts,
"enable-manual-sync PROJECT ID",
"Enable manual sync for a sync window",
"Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Enabling manual sync for a general case
argocd proj windows enable-manual-sync PROJECT ID
@ -107,35 +129,48 @@ argocd proj windows enable-manual-sync default 2
#Enabling manual sync with a custom message
argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for i, window := range proj.Spec.SyncWindows {
if id == i {
window.ManualSync = true
}
}
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
func(window *v1alpha1.SyncWindow) {
window.ManualSync = true
},
}
return command
)
}
// NewProjectWindowsDisableSyncOverrunCommand returns a new instance of an `argocd proj windows disable-sync-overrun` command
func NewProjectWindowsDisableSyncOverrunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"disable-sync-overrun PROJECT ID",
"Disable sync overrun for a sync window",
"Disable sync overrun for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Disable sync overrun for a sync window for the Project
argocd proj windows disable-sync-overrun PROJECT ID
#Disabling sync overrun for a window set on the default project with Id 0
argocd proj windows disable-sync-overrun default 0`,
func(window *v1alpha1.SyncWindow) {
window.SyncOverrun = false
},
)
}
// NewProjectWindowsEnableSyncOverrunCommand returns a new instance of an `argocd proj windows enable-sync-overrun` command
func NewProjectWindowsEnableSyncOverrunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"enable-sync-overrun PROJECT ID",
"Enable sync overrun for a sync window",
"Enable sync overrun for a sync window. When enabled on a deny window, syncs that started before the deny window will be allowed to continue. When enabled on an allow window, syncs that started during the allow window can continue after the window ends. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Enable sync overrun for a sync window
argocd proj windows enable-sync-overrun PROJECT ID
#Enabling sync overrun for a window set on the default project with Id 2
argocd proj windows enable-sync-overrun default 2`,
func(window *v1alpha1.SyncWindow) {
window.SyncOverrun = true
},
)
}
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows add` command
@ -148,6 +183,7 @@ func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *
namespaces []string
clusters []string
manualSync bool
syncOverrun bool
timeZone string
andOperator bool
description string
@ -164,7 +200,7 @@ argocd proj windows add PROJECT \
--applications "*" \
--description "Ticket 123"
#Add a deny sync window with the ability to manually sync.
#Add a deny sync window with the ability to manually sync and sync overrun.
argocd proj windows add PROJECT \
--kind deny \
--schedule "30 10 * * *" \
@ -173,8 +209,8 @@ argocd proj windows add PROJECT \
--namespaces "default,\\*-prod" \
--clusters "prod,staging" \
--manual-sync \
--description "Ticket 123"
`,
--sync-overrun \
--description "Ticket 123"`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
@ -189,7 +225,7 @@ argocd proj windows add PROJECT \
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync, timeZone, andOperator, description)
err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync, timeZone, andOperator, description, syncOverrun)
errors.CheckError(err)
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
@ -203,6 +239,7 @@ argocd proj windows add PROJECT \
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
command.Flags().BoolVar(&manualSync, "manual-sync", false, "Allow manual syncs for both deny and allow windows")
command.Flags().BoolVar(&syncOverrun, "sync-overrun", false, "Allow syncs to continue: for deny windows, syncs that started before the window; for allow windows, syncs that started during the window")
command.Flags().StringVar(&timeZone, "time-zone", "UTC", "Time zone of the sync window")
command.Flags().BoolVar(&andOperator, "use-and-operator", false, "Use AND operator for matching applications, namespaces and clusters instead of the default OR operator")
command.Flags().StringVar(&description, "description", "", `Sync window description`)
@ -362,7 +399,7 @@ argocd proj windows list test-project`,
func printSyncWindows(proj *v1alpha1.AppProject) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var fmtStr string
headers := []any{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "TIMEZONE", "USEANDOPERATOR"}
headers := []any{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "SYNCOVERRUN", "TIMEZONE", "USEANDOPERATOR"}
fmtStr = strings.Repeat("%s\t", len(headers)) + "\n"
fmt.Fprintf(w, fmtStr, headers...)
if proj.Spec.SyncWindows.HasWindows() {
@ -378,6 +415,7 @@ func printSyncWindows(proj *v1alpha1.AppProject) {
formatListOutput(window.Namespaces),
formatListOutput(window.Clusters),
formatBoolEnabledOutput(window.ManualSync),
formatBoolEnabledOutput(window.SyncOverrun),
window.TimeZone,
formatBoolEnabledOutput(window.UseAndOperator),
}

View file

@ -1,6 +1,11 @@
package commands
import (
"bytes"
"io"
"os"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -11,30 +16,229 @@ import (
)
func TestPrintSyncWindows(t *testing.T) {
proj := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "test-project"},
Spec: v1alpha1.AppProjectSpec{
SyncWindows: v1alpha1.SyncWindows{
{
Kind: "allow",
Schedule: "* * * * *",
Duration: "1h",
Applications: []string{"app1"},
Namespaces: []string{"ns1"},
Clusters: []string{"cluster1"},
ManualSync: true,
UseAndOperator: true,
tests := []struct {
name string
project *v1alpha1.AppProject
expectedHeader []string
expectedRows [][]string
}{
{
name: "Project with multiple sync windows including syncOverrun",
project: &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "test-project",
},
Spec: v1alpha1.AppProjectSpec{
SyncWindows: v1alpha1.SyncWindows{
{
Kind: "allow",
Schedule: "0 0 * * *",
Duration: "1h",
Applications: []string{"app1", "app2"},
Namespaces: []string{"default"},
Clusters: []string{"cluster1"},
ManualSync: false,
SyncOverrun: false,
TimeZone: "UTC",
UseAndOperator: false,
},
{
Kind: "deny",
Schedule: "0 12 * * *",
Duration: "2h",
Applications: []string{"*"},
Namespaces: []string{"production"},
Clusters: []string{"*"},
ManualSync: true,
SyncOverrun: true,
TimeZone: "America/New_York",
UseAndOperator: true,
},
},
},
},
expectedHeader: []string{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "SYNCOVERRUN", "TIMEZONE", "USEANDOPERATOR"},
expectedRows: [][]string{
{"0", "Inactive", "allow", "0 0 * * *", "1h", "app1,app2", "default", "cluster1", "Disabled", "Disabled", "UTC", "Disabled"},
{"1", "Inactive", "deny", "0 12 * * *", "2h", "*", "production", "*", "Enabled", "Enabled", "America/New_York", "Enabled"},
},
},
{
name: "Project with empty sync window lists",
project: &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "test-project",
},
Spec: v1alpha1.AppProjectSpec{
SyncWindows: v1alpha1.SyncWindows{
{
Kind: "allow",
Schedule: "0 1 * * *",
Duration: "30m",
Applications: []string{},
Namespaces: []string{},
Clusters: []string{},
ManualSync: false,
SyncOverrun: false,
TimeZone: "UTC",
UseAndOperator: false,
},
},
},
},
expectedHeader: []string{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "SYNCOVERRUN", "TIMEZONE", "USEANDOPERATOR"},
expectedRows: [][]string{
{"0", "Inactive", "allow", "0 1 * * *", "30m", "-", "-", "-", "Disabled", "Disabled", "UTC", "Disabled"},
},
},
{
name: "Project with no sync windows",
project: &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "test-project",
},
Spec: v1alpha1.AppProjectSpec{
SyncWindows: v1alpha1.SyncWindows{},
},
},
expectedHeader: []string{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "SYNCOVERRUN", "TIMEZONE", "USEANDOPERATOR"},
expectedRows: [][]string{},
},
}
output, err := captureOutput(func() error {
printSyncWindows(proj)
return nil
})
require.NoError(t, err)
t.Log(output)
assert.Contains(t, output, "ID STATUS KIND SCHEDULE DURATION APPLICATIONS NAMESPACES CLUSTERS MANUALSYNC TIMEZONE USEANDOPERATOR")
assert.Contains(t, output, "0 Active allow * * * * * 1h app1 ns1 cluster1 Enabled Enabled")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call the function
printSyncWindows(tt.project)
// Restore stdout
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
require.NoError(t, err)
output := buf.String()
// Parse the table output
lines := strings.Split(strings.TrimSpace(output), "\n")
assert.GreaterOrEqual(t, len(lines), 1, "Should have at least a header line")
// Parse header line (split by whitespace for headers since they don't contain spaces)
headerLine := lines[0]
headerFields := strings.Fields(headerLine)
assert.Len(t, headerFields, len(tt.expectedHeader), "Header should have correct number of columns")
assert.Equal(t, tt.expectedHeader, headerFields, "Header columns should match expected")
// Parse data rows
dataLines := lines[1:]
assert.Len(t, dataLines, len(tt.expectedRows), "Should have expected number of data rows")
for i, dataLine := range dataLines {
// Split by 2 or more spaces (tabwriter output uses multiple spaces as separators)
re := regexp.MustCompile(`\s{2,}`)
fields := re.Split(strings.TrimSpace(dataLine), -1)
assert.Len(t, fields, len(tt.expectedRows[i]), "Row %d should have correct number of columns", i)
for j, expectedValue := range tt.expectedRows[i] {
assert.Equal(t, expectedValue, fields[j], "Row %d, column %d should match expected value", i, j)
}
}
})
}
}
func TestFormatListOutput(t *testing.T) {
tests := []struct {
name string
input []string
expected string
}{
{
name: "Empty list",
input: []string{},
expected: "-",
},
{
name: "Single item",
input: []string{"app1"},
expected: "app1",
},
{
name: "Multiple items",
input: []string{"app1", "app2", "app3"},
expected: "app1,app2,app3",
},
{
name: "Wildcard",
input: []string{"*"},
expected: "*",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatListOutput(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFormatBoolOutput(t *testing.T) {
tests := []struct {
name string
input bool
expected string
}{
{
name: "Active",
input: true,
expected: "Active",
},
{
name: "Inactive",
input: false,
expected: "Inactive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatBoolOutput(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFormatBoolEnabledOutput(t *testing.T) {
tests := []struct {
name string
input bool
expected string
}{
{
name: "Enabled",
input: true,
expected: "Enabled",
},
{
name: "Disabled",
input: false,
expected: "Disabled",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatBoolEnabledOutput(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}

View file

@ -1885,7 +1885,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
app.Status.Summary = tree.GetSummary(app)
}
canSync, _ := project.Spec.SyncWindows.Matches(app).CanSync(false)
canSync, _ := project.Spec.SyncWindows.Matches(app).CanSync(false, nil)
if canSync {
syncErrCond, opDuration := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources, compareResult.revisionsMayHaveChanges)
setOpDuration = opDuration

View file

@ -542,10 +542,15 @@ func delayBetweenSyncWaves(_ common.SyncPhase, _ int, finalWave bool) error {
func syncWindowPreventsSync(app *v1alpha1.Application, proj *v1alpha1.AppProject) (bool, error) {
window := proj.Spec.SyncWindows.Matches(app)
isManual := false
var operationStartTime *time.Time
if app.Status.OperationState != nil {
isManual = !app.Status.OperationState.Operation.InitiatedBy.Automated
if !app.Status.OperationState.StartedAt.IsZero() {
t := app.Status.OperationState.StartedAt.Time
operationStartTime = &t
}
}
canSync, err := window.CanSync(isManual)
canSync, err := window.CanSync(isManual, operationStartTime)
if err != nil {
// prevents sync because sync window has an error
return true, err

View file

@ -89,11 +89,18 @@ spec:
applications:
- '*-prod'
manualSync: true
- kind: allow
schedule: '0 9 * * *'
duration: 8h
applications:
- '*'
syncOverrun: true # Syncs started during this window can continue after window ends
- kind: deny
schedule: '0 22 * * *'
duration: 1h
namespaces:
- default
syncOverrun: true # Syncs started before this window can continue during the window
- kind: allow
schedule: '0 23 * * *'
duration: 1h

View file

@ -68,7 +68,9 @@ argocd proj windows list <project-name>
* [argocd proj windows add](argocd_proj_windows_add.md) - Add a sync window to a project
* [argocd proj windows delete](argocd_proj_windows_delete.md) - Delete a sync window from a project. Requires ID which can be found by running "argocd proj windows list PROJECT"
* [argocd proj windows disable-manual-sync](argocd_proj_windows_disable-manual-sync.md) - Disable manual sync for a sync window
* [argocd proj windows disable-sync-overrun](argocd_proj_windows_disable-sync-overrun.md) - Disable sync overrun for a sync window
* [argocd proj windows enable-manual-sync](argocd_proj_windows_enable-manual-sync.md) - Enable manual sync for a sync window
* [argocd proj windows enable-sync-overrun](argocd_proj_windows_enable-sync-overrun.md) - Enable sync overrun for a sync window
* [argocd proj windows list](argocd_proj_windows_list.md) - List project sync windows
* [argocd proj windows update](argocd_proj_windows_update.md) - Update a project sync window

View file

@ -20,7 +20,7 @@ argocd proj windows add PROJECT \
--applications "*" \
--description "Ticket 123"
#Add a deny sync window with the ability to manually sync.
#Add a deny sync window with the ability to manually sync and sync overrun.
argocd proj windows add PROJECT \
--kind deny \
--schedule "30 10 * * *" \
@ -29,8 +29,8 @@ argocd proj windows add PROJECT \
--namespaces "default,\\*-prod" \
--clusters "prod,staging" \
--manual-sync \
--sync-overrun \
--description "Ticket 123"
```
### Options
@ -45,6 +45,7 @@ argocd proj windows add PROJECT \
--manual-sync Allow manual syncs for both deny and allow windows
--namespaces strings Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\*-prod)
--schedule string Sync window schedule in cron format. (e.g. --schedule "0 22 * * *")
--sync-overrun Allow syncs to continue: for deny windows, syncs that started before the window; for allow windows, syncs that started during the window
--time-zone string Time zone of the sync window (default "UTC")
--use-and-operator Use AND operator for matching applications, namespaces and clusters instead of the default OR operator
```

View file

@ -0,0 +1,66 @@
# `argocd proj windows disable-sync-overrun` Command Reference
## argocd proj windows disable-sync-overrun
Disable sync overrun for a sync window
### Synopsis
Disable sync overrun for a sync window. Requires ID which can be found by running "argocd proj windows list PROJECT"
```
argocd proj windows disable-sync-overrun PROJECT ID [flags]
```
### Examples
```
#Disable sync overrun for a sync window for the Project
argocd proj windows disable-sync-overrun PROJECT ID
#Disabling sync overrun for a window set on the default project with Id 0
argocd proj windows disable-sync-overrun default 0
```
### Options
```
-h, --help help for disable-sync-overrun
```
### Options inherited from parent commands
```
--argocd-context string The name of the Argo-CD server context to use
--auth-token string Authentication token; set this or the ARGOCD_AUTH_TOKEN environment variable
--client-crt string Client certificate file
--client-crt-key string Client certificate key file
--config string Path to Argo CD config (default "/home/user/.config/argocd/config")
--controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller")
--core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server
--grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.
--grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.
-H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)
--http-retry-max int Maximum number of retries to establish http connection to Argo CD server
--insecure Skip server certificate and domain verification
--kube-context string Directs the command to the given kube-context
--logformat string Set the logging format. One of: json|text (default "json")
--loglevel string Set the logging level. One of: debug|info|warn|error (default "info")
--plaintext Disable TLS
--port-forward Connect to a random argocd-server port using port forwarding
--port-forward-namespace string Namespace name which should be used for port forwarding
--prompts-enabled Force optional interactive prompts to be enabled or disabled, overriding local configuration. If not specified, the local configuration value will be used, which is false by default.
--redis-compress string Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none) (default "gzip")
--redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy")
--redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis")
--repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server")
--server string Argo CD server address
--server-crt string Server certificate file
--server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server")
```
### SEE ALSO
* [argocd proj windows](argocd_proj_windows.md) - Manage a project's sync windows

View file

@ -0,0 +1,66 @@
# `argocd proj windows enable-sync-overrun` Command Reference
## argocd proj windows enable-sync-overrun
Enable sync overrun for a sync window
### Synopsis
Enable sync overrun for a sync window. When enabled on a deny window, syncs that started before the deny window will be allowed to continue. When enabled on an allow window, syncs that started during the allow window can continue after the window ends. Requires ID which can be found by running "argocd proj windows list PROJECT"
```
argocd proj windows enable-sync-overrun PROJECT ID [flags]
```
### Examples
```
#Enable sync overrun for a sync window
argocd proj windows enable-sync-overrun PROJECT ID
#Enabling sync overrun for a window set on the default project with Id 2
argocd proj windows enable-sync-overrun default 2
```
### Options
```
-h, --help help for enable-sync-overrun
```
### Options inherited from parent commands
```
--argocd-context string The name of the Argo-CD server context to use
--auth-token string Authentication token; set this or the ARGOCD_AUTH_TOKEN environment variable
--client-crt string Client certificate file
--client-crt-key string Client certificate key file
--config string Path to Argo CD config (default "/home/user/.config/argocd/config")
--controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller")
--core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server
--grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.
--grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.
-H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)
--http-retry-max int Maximum number of retries to establish http connection to Argo CD server
--insecure Skip server certificate and domain verification
--kube-context string Directs the command to the given kube-context
--logformat string Set the logging format. One of: json|text (default "json")
--loglevel string Set the logging level. One of: debug|info|warn|error (default "info")
--plaintext Disable TLS
--port-forward Connect to a random argocd-server port using port forwarding
--port-forward-namespace string Namespace name which should be used for port forwarding
--prompts-enabled Force optional interactive prompts to be enabled or disabled, overriding local configuration. If not specified, the local configuration value will be used, which is false by default.
--redis-compress string Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none) (default "gzip")
--redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy")
--redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis")
--repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server")
--server string Argo CD server address
--server-crt string Server certificate file
--server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server")
```
### SEE ALSO
* [argocd proj windows](argocd_proj_windows.md) - Manage a project's sync windows

View file

@ -28,8 +28,8 @@ then the Application is affected by the Sync Window.
## Effect of Sync Windows
These windows affect the running of both manual and automated syncs but allow an override
for manual syncs which is useful if you are only interested in preventing automated syncs or if you need to temporarily
These windows affect the running of both manual and automated syncs but allow an override
for manual syncs which is useful if you are only interested in preventing automated syncs or if you need to temporarily
override a window to perform a sync.
The windows work in the following way:
@ -39,6 +39,45 @@ The windows work in the following way:
- If there are any `deny` windows matching an application then all syncs will be denied when the `deny` windows are active.
- If there is an active matching `allow` and an active matching `deny` then syncs will be denied as `deny` windows override `allow` windows.
### Sync Overrun
The `syncOverrun` option allows automatic syncs that are already running to continue even when they transition out of their allowed window. This is particularly useful when you want to prevent new syncs from starting during maintenance windows but don't want to interrupt syncs that are already in progress.
Sync overrun can be configured on both `allow` and `deny` windows:
#### Deny Window Overrun
When `syncOverrun` is enabled on a deny window:
- Syncs that started **before** the deny window became active will be allowed to complete
- New syncs will still be blocked during the deny window
- **All active deny windows must have syncOverrun enabled** for the overrun to be allowed
#### Allow Window Overrun
When `syncOverrun` is enabled on an allow window:
- Syncs that started **during** the allow window will be allowed to continue even after the window ends
- This is useful for long-running syncs that may extend beyond the scheduled window
- **All inactive allow windows must have syncOverrun enabled** for the overrun to be allowed
- The sync can continue as long as:
- No deny window without overrun becomes active
- If a deny window becomes active, it must also have syncOverrun enabled
#### Transition Scenarios
Here are some common scenarios and their behavior:
1. **No window → Deny with overrun**: Sync continues (deny supports overrun, sync was permitted when it started)
1. **No window → Deny without overrun**: Sync is **blocked** (deny doesn't support overrun)
1. **Allow with overrun → Deny with overrun**: Sync continues (both windows support overrun)
1. **Allow with overrun → Deny without overrun**: Sync is **blocked** (deny doesn't support overrun)
1. **Allow without overrun → Deny with overrun**: Sync is **allowed** (deny supports overrun, sync was permitted when it started)
1. **Allow without overrun → Deny without overrun**: Sync is **blocked** (neither supports overrun)
1. **Allow with overrun → Allow ends**: Sync continues (overrun enabled on original allow window)
1. **Multiple allows with overrun → All end**: Sync continues (all allow windows have overrun)
1. **Multiple allows, one without overrun → All end**: Sync is **blocked** (not all allow windows have overrun)
The UI and the CLI will both display the state of the sync windows. The UI has a panel which will display different colours depending
on the state. The colours are as follows. `Red: sync denied`, `Orange: manual allowed` and `Green: sync allowed`.
@ -76,8 +115,28 @@ argocd proj windows add PROJECT \
--applications "*"
```
To create a window with sync overrun enabled (allowing in-progress syncs to continue):
```bash
# Allow window with overrun - syncs can continue after window ends
argocd proj windows add PROJECT \
--kind allow \
--schedule "0 9 * * *" \
--duration 8h \
--applications "*" \
--sync-overrun
# Deny window with overrun - in-progress syncs can continue during deny window
argocd proj windows add PROJECT \
--kind deny \
--schedule "0 22 * * *" \
--duration 1h \
--applications "*" \
--sync-overrun
```
Alternatively, they can be created directly in the `AppProject` manifest:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
@ -91,12 +150,19 @@ spec:
applications:
- '*-prod'
manualSync: true
- kind: allow
schedule: '0 9 * * *'
duration: 8h
applications:
- '*'
syncOverrun: true # Allow syncs to continue after window ends
- kind: deny
schedule: '0 22 * * *'
timeZone: "Europe/Amsterdam"
duration: 1h
namespaces:
- default
syncOverrun: true # Allow in-progress syncs to continue during deny window
- kind: allow
schedule: '0 23 * * *'
duration: 1h
@ -112,12 +178,24 @@ using the CLI, UI or directly in the `AppProject` manifest:
argocd proj windows enable-manual-sync PROJECT ID
```
To disable
To disable:
```bash
argocd proj windows disable-manual-sync PROJECT ID
```
Similarly, you can enable or disable sync overrun for existing windows:
```bash
argocd proj windows enable-sync-overrun PROJECT ID
```
To disable:
```bash
argocd proj windows disable-sync-overrun PROJECT ID
```
Windows can be listed using the CLI or viewed in the UI:
```bash
@ -125,11 +203,11 @@ argocd proj windows list PROJECT
```
```bash
ID STATUS KIND SCHEDULE DURATION APPLICATIONS NAMESPACES CLUSTERS MANUALSYNC
0 Active allow * * * * * 1h - - prod1 Disabled
1 Inactive deny * * * * 1 3h - default - Disabled
2 Inactive allow 1 2 * * * 1h prod-* - - Enabled
3 Active deny * * * * * 1h - default - Disabled
ID STATUS KIND SCHEDULE DURATION APPLICATIONS NAMESPACES CLUSTERS MANUALSYNC SYNCOVERRUN TIMEZONE USEANDOPERATOR
0 Active allow * * * * * 1h - - prod1 Disabled Disabled UTC Disabled
1 Inactive deny * * * * 1 3h - default - Disabled Enabled UTC Disabled
2 Inactive allow 1 2 * * * 1h prod-* - - Enabled Disabled UTC Disabled
3 Active deny * * * * * 1h - default - Disabled Disabled UTC Disabled
```
All fields of a window can be updated using either the CLI or UI. The `applications`, `namespaces` and `clusters` fields

View file

@ -30528,6 +30528,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

View file

@ -30528,6 +30528,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

View file

@ -329,6 +329,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

View file

@ -30528,6 +30528,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

View file

@ -30528,6 +30528,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

View file

@ -30528,6 +30528,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

View file

@ -30528,6 +30528,12 @@ spec:
description: Schedule is the time the window will begin, specified
in cron format
type: string
syncOverrun:
description: |-
SyncOverrun allows ongoing syncs to continue in two scenarios:
For deny windows: allows syncs that started before the deny window became active to continue running
For allow windows: allows syncs that started during the allow window to continue after the window ends
type: boolean
timeZone:
description: TimeZone of the sync that will be applied to the
schedule

File diff suppressed because it is too large Load diff

View file

@ -2815,6 +2815,11 @@ message SyncWindow {
// Description of the sync that will be applied to the schedule, can be used to add any information such as a ticket number for example
optional string description = 10;
// SyncOverrun allows ongoing syncs to continue in two scenarios:
// For deny windows: allows syncs that started before the deny window became active to continue running
// For allow windows: allows syncs that started during the allow window to continue after the window ends
optional bool syncOverrun = 11;
}
// TLSClientConfig contains settings to enable transport layer security

View file

@ -8457,6 +8457,13 @@ func schema_pkg_apis_application_v1alpha1_SyncWindow(ref common.ReferenceCallbac
Format: "",
},
},
"syncOverrun": {
SchemaProps: spec.SchemaProps{
Description: "SyncOverrun allows syncs that started before this deny window to continue running",
Type: []string{"boolean"},
Format: "",
},
},
},
},
},

View file

@ -2837,6 +2837,10 @@ type SyncWindow struct {
UseAndOperator bool `json:"andOperator,omitempty" protobuf:"bytes,9,opt,name=andOperator"`
// Description of the sync that will be applied to the schedule, can be used to add any information such as a ticket number for example
Description string `json:"description,omitempty" protobuf:"bytes,10,opt,name=description"`
// SyncOverrun allows ongoing syncs to continue in two scenarios:
// For deny windows: allows syncs that started before the deny window became active to continue running
// For allow windows: allows syncs that started during the allow window to continue after the window ends
SyncOverrun bool `json:"syncOverrun,omitempty" protobuf:"bytes,11,opt,name=syncOverrun"`
}
// HasWindows returns true if SyncWindows has one or more SyncWindow
@ -2934,7 +2938,7 @@ func (w *SyncWindow) scheduleOffsetByTimeZone() time.Duration {
}
// AddWindow adds a sync window with the given parameters to the AppProject
func (spec *AppProjectSpec) AddWindow(knd string, sch string, dur string, app []string, ns []string, cl []string, ms bool, timeZone string, andOperator bool, description string) error {
func (spec *AppProjectSpec) AddWindow(knd string, sch string, dur string, app []string, ns []string, cl []string, ms bool, timeZone string, andOperator bool, description string, syncOverrun bool) error {
if knd == "" || sch == "" || dur == "" {
return errors.New("cannot create window: require kind, schedule, duration and one or more of applications, namespaces and clusters")
}
@ -2947,6 +2951,7 @@ func (spec *AppProjectSpec) AddWindow(knd string, sch string, dur string, app []
TimeZone: timeZone,
UseAndOperator: andOperator,
Description: description,
SyncOverrun: syncOverrun,
}
if len(app) > 0 {
@ -3062,7 +3067,12 @@ func (w *SyncWindows) Matches(app *Application) *SyncWindows {
}
// CanSync returns true if a sync window currently allows a sync. isManual indicates whether the sync has been triggered manually.
func (w *SyncWindows) CanSync(isManual bool) (bool, error) {
// The operationStartTime parameter supports sync overrun functionality, which allows ongoing syncs to continue in two scenarios:
// 1. When a deny window becomes active: If the operation started when sync was allowed and the deny window has syncOverrun enabled,
// the sync can continue even though a deny window is now active.
// 2. When an allow window ends: If the operation started during an allow window with syncOverrun enabled, the sync can continue
// even after the allow window has ended (and no other allow windows are active).
func (w *SyncWindows) CanSync(isManual bool, operationStartTime *time.Time) (bool, error) {
if !w.HasWindows() {
return true, nil
}
@ -3077,6 +3087,18 @@ func (w *SyncWindows) CanSync(isManual bool) (bool, error) {
if isManual && manualEnabled {
return true, nil
}
// Check if operation started before deny window and overrun is allowed
if operationStartTime != nil && !operationStartTime.IsZero() && active.denyAllowsOverrun() {
wasAllowed, err := w.canSyncAtTime(isManual, *operationStartTime)
if err != nil {
return false, err
}
if wasAllowed {
return true, nil // Allow sync to continue (overrun into deny window)
}
}
return false, nil
}
@ -3092,6 +3114,18 @@ func (w *SyncWindows) CanSync(isManual bool) (bool, error) {
if isManual && inactiveAllows.manualEnabled() {
return true, nil
}
// Check if operation started during an allow window and overrun is allowed
if operationStartTime != nil && !operationStartTime.IsZero() && inactiveAllows.inactiveAllowsAllowOverrun() {
wasAllowed, err := w.canSyncAtTime(isManual, *operationStartTime)
if err != nil {
return false, err
}
if wasAllowed {
return true, nil // Allow sync to continue (overrun out of allow window)
}
}
return false, nil
}
@ -3149,6 +3183,82 @@ func (w *SyncWindows) manualEnabled() bool {
return true
}
// denyAllowsOverrun will iterate over the deny SyncWindows and return true if all deny windows have
// SyncOverrun set to true. Returns false if it finds at least one deny window with
// SyncOverrun set to false. This is used to determine if a sync can continue when a deny
// window becomes active after the operation started.
func (w *SyncWindows) denyAllowsOverrun() bool {
if !w.HasWindows() {
return false
}
hasDeny := false
for _, s := range *w {
if s.Kind == "deny" {
hasDeny = true
if !s.SyncOverrun {
return false
}
}
}
return hasDeny
}
// inactiveAllowsAllowOverrun checks if all inactive allow windows have SyncOverrun enabled.
// This is used to determine if a sync can continue after an allow window has ended.
// Similar to allowsOverrun() for deny windows, ALL allow windows must have SyncOverrun enabled.
func (w *SyncWindows) inactiveAllowsAllowOverrun() bool {
if !w.HasWindows() {
return false
}
hasAllow := false
for _, s := range *w {
if s.Kind == "allow" {
hasAllow = true
if !s.SyncOverrun {
return false
}
}
}
return hasAllow
}
// canSyncAtTime checks if a sync would have been allowed at a specific time
func (w *SyncWindows) canSyncAtTime(isManual bool, checkTime time.Time) (bool, error) {
if !w.HasWindows() {
return true, nil
}
active, err := w.active(checkTime)
if err != nil {
return false, fmt.Errorf("invalid sync windows: %w", err)
}
hasActiveDeny, manualEnabled := active.hasDeny()
if hasActiveDeny {
if isManual && manualEnabled {
return true, nil
}
return false, nil
}
if active.hasAllow() {
return true, nil
}
inactiveAllows, err := w.inactiveAllows(checkTime)
if err != nil {
return false, fmt.Errorf("invalid sync windows: %w", err)
}
if inactiveAllows.HasWindows() {
if isManual && inactiveAllows.manualEnabled() {
return true, nil
}
return false, nil
}
return true, nil
}
// Active returns true if the sync window is currently active
func (w SyncWindow) Active() (bool, error) {
return w.active(time.Now())

File diff suppressed because it is too large Load diff

View file

@ -2050,7 +2050,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR
s.inferResourcesStatusHealth(a)
canSync, err := proj.Spec.SyncWindows.Matches(a).CanSync(true)
canSync, err := proj.Spec.SyncWindows.Matches(a).CanSync(true, nil)
if err != nil {
return a, status.Errorf(codes.PermissionDenied, "cannot sync: invalid sync window: %v", err)
}
@ -2846,7 +2846,7 @@ func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.A
}
windows := proj.Spec.SyncWindows.Matches(a)
sync, err := windows.CanSync(true)
sync, err := windows.CanSync(true, nil)
if err != nil {
return nil, fmt.Errorf("invalid sync windows: %w", err)
}

View file

@ -578,6 +578,61 @@ func TestGetVirtualProjectMatch(t *testing.T) {
assert.ErrorContains(t, err, "blocked by sync window")
}
func TestSyncWindowSyncOverrun(t *testing.T) {
fixture.EnsureCleanState(t)
projectName := "proj-" + strconv.FormatInt(time.Now().Unix(), 10)
_, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Create(
t.Context(), &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: projectName}}, metav1.CreateOptions{})
require.NoError(t, err, "Unable to create project")
// Test adding a sync window with --sync-overrun flag
_, err = fixture.RunCli("proj", "windows", "add", projectName,
"--kind", "deny",
"--schedule", "0 0 * * *",
"--duration", "1h",
"--applications", "*",
"--sync-overrun")
require.NoError(t, err, "Unable to add sync window with sync overrun")
proj, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Get(t.Context(), projectName, metav1.GetOptions{})
require.NoError(t, err)
assert.Len(t, proj.Spec.SyncWindows, 1)
assert.True(t, proj.Spec.SyncWindows[0].SyncOverrun, "SyncOverrun should be true after adding with flag")
// Test disabling sync overrun
_, err = fixture.RunCli("proj", "windows", "disable-sync-overrun", projectName, "0")
require.NoError(t, err, "Unable to disable sync overrun")
proj, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Get(t.Context(), projectName, metav1.GetOptions{})
require.NoError(t, err)
assert.False(t, proj.Spec.SyncWindows[0].SyncOverrun, "SyncOverrun should be false after disabling")
// Test enabling sync overrun
_, err = fixture.RunCli("proj", "windows", "enable-sync-overrun", projectName, "0")
require.NoError(t, err, "Unable to enable sync overrun")
proj, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Get(t.Context(), projectName, metav1.GetOptions{})
require.NoError(t, err)
assert.True(t, proj.Spec.SyncWindows[0].SyncOverrun, "SyncOverrun should be true after enabling")
// Add a second window without sync overrun to test multiple windows
_, err = fixture.RunCli("proj", "windows", "add", projectName,
"--kind", "deny",
"--schedule", "12 0 * * *",
"--duration", "2h",
"--applications", "*")
require.NoError(t, err, "Unable to add second sync window")
proj, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Get(t.Context(), projectName, metav1.GetOptions{})
require.NoError(t, err)
assert.Len(t, proj.Spec.SyncWindows, 2)
assert.True(t, proj.Spec.SyncWindows[0].SyncOverrun, "First window should still have SyncOverrun enabled")
assert.False(t, proj.Spec.SyncWindows[1].SyncOverrun, "Second window should not have SyncOverrun enabled")
assertProjHasEvent(t, proj, "update", argo.EventReasonResourceUpdated)
}
func TestAddProjectDestinationServiceAccount(t *testing.T) {
fixture.EnsureCleanState(t)

View file

@ -258,6 +258,12 @@ export const ProjectDetails: React.FC<RouteComponentProps<{name: string}> & {obj
MANUALSYNC
{helpTip('If the window allows manual syncs')}
</div>
<div className='columns small-8-elements'>
SYNC OVERRUN
{helpTip(
'Allows syncs to continue: for deny windows, syncs that started before the window; for allow windows, syncs that started during the window'
)}
</div>
<div className='columns small-8-elements'>
USE AND OPERATOR
{helpTip('Use AND operator while selecting the apps that match the configured selectors')}
@ -283,6 +289,7 @@ export const ProjectDetails: React.FC<RouteComponentProps<{name: string}> & {obj
<div className='columns small-8-elements'>{(window.namespaces || ['-']).join(',')}</div>
<div className='columns small-8-elements'>{(window.clusters || ['-']).join(',')}</div>
<div className='columns small-8-elements'>{window.manualSync ? 'Enabled' : 'Disabled'}</div>
<div className='columns small-8-elements'>{window.syncOverrun ? 'Enabled' : 'Disabled'}</div>
<div className='columns small-8-elements'>{window.andOperator ? 'Enabled' : 'Disabled'}</div>
<div className='columns small-8-elements'>{window.description || ''}</div>
</div>

View file

@ -67,6 +67,9 @@ export const ProjectSyncWindowsEditPanel = (props: ProjectSyncWindowsEditPanelPr
<div className='argo-form-row'>
<FormField formApi={api} label='Enable manual sync' field='window.manualSync' component={CheckboxField} />
</div>
<div className='argo-form-row'>
<FormField formApi={api} label='Enable sync overrun' field='window.syncOverrun' component={CheckboxField} />
</div>
<div className='argo-form-row'>
<FormField
formApi={api}

View file

@ -893,6 +893,7 @@ export interface SyncWindow {
timeZone: string;
andOperator: boolean;
description: string;
syncOverrun: boolean;
}
export interface Project {