mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 08:57:17 +00:00
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
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:
parent
9c8ae9a294
commit
611fcb012c
29 changed files with 2418 additions and 915 deletions
4
assets/swagger.json
generated
4
assets/swagger.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
docs/user-guide/commands/argocd_proj_windows.md
generated
2
docs/user-guide/commands/argocd_proj_windows.md
generated
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
66
docs/user-guide/commands/argocd_proj_windows_disable-sync-overrun.md
generated
Normal file
66
docs/user-guide/commands/argocd_proj_windows_disable-sync-overrun.md
generated
Normal 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
|
||||
|
||||
66
docs/user-guide/commands/argocd_proj_windows_enable-sync-overrun.md
generated
Normal file
66
docs/user-guide/commands/argocd_proj_windows_enable-sync-overrun.md
generated
Normal 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
|
||||
|
||||
|
|
@ -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,6 +115,26 @@ 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/core-install-with-hydrator.yaml
generated
6
manifests/core-install-with-hydrator.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/core-install.yaml
generated
6
manifests/core-install.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/crds/appproject-crd.yaml
generated
6
manifests/crds/appproject-crd.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/ha/install-with-hydrator.yaml
generated
6
manifests/ha/install-with-hydrator.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/ha/install.yaml
generated
6
manifests/ha/install.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/install-with-hydrator.yaml
generated
6
manifests/install-with-hydrator.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
6
manifests/install.yaml
generated
6
manifests/install.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
1623
pkg/apis/application/v1alpha1/generated.pb.go
generated
1623
pkg/apis/application/v1alpha1/generated.pb.go
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -893,6 +893,7 @@ export interface SyncWindow {
|
|||
timeZone: string;
|
||||
andOperator: boolean;
|
||||
description: string;
|
||||
syncOverrun: boolean;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
|
|
|
|||
Loading…
Reference in a new issue