@@ -32,6 +32,36 @@ import (
32
32
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33
33
)
34
34
35
+ // orderApplications reorders apps by schedule policy: lru (least-recent success first),
36
+ // fail-first (recent failures first), with optional cooldown to deprioritize recently
37
+ // successful apps.
38
+ func orderApplications (names []string , appList map [string ]argocd.ApplicationImages , state * argocd.SyncIterationState , cfg * ImageUpdaterConfig ) []string {
39
+ stats := state .GetStats ()
40
+ type item struct { name string ; score int64 }
41
+ items := make ([]item , 0 , len (names ))
42
+ now := time .Now ()
43
+ for _ , n := range names {
44
+ s := stats [n ]
45
+ score := int64 (0 )
46
+ switch cfg .Schedule {
47
+ case "lru" :
48
+ // Older success => higher priority (lower score)
49
+ if ! s .LastSuccess .IsZero () { score -= int64 (now .Sub (s .LastSuccess ).Milliseconds ()) }
50
+ case "fail-first" :
51
+ score += int64 (s .FailCount ) * 1_000_000 // dominate by failures
52
+ if ! s .LastAttempt .IsZero () { score -= int64 (now .Sub (s .LastAttempt ).Milliseconds ()) }
53
+ }
54
+ if cfg .Cooldown > 0 && ! s .LastSuccess .IsZero () && now .Sub (s .LastSuccess ) < cfg .Cooldown {
55
+ score -= 1 // slight deprioritization
56
+ }
57
+ items = append (items , item {name : n , score : score })
58
+ }
59
+ sort .Slice (items , func (i , j int ) bool { return items [i ].score > items [j ].score })
60
+ out := make ([]string , len (items ))
61
+ for i := range items { out [i ] = items [i ].name }
62
+ return out
63
+ }
64
+
35
65
// newRunCommand implements "run" command
36
66
func newRunCommand () * cobra.Command {
37
67
var cfg * ImageUpdaterConfig = & ImageUpdaterConfig {}
@@ -60,7 +90,7 @@ func newRunCommand() *cobra.Command {
60
90
return fmt .Errorf ("--max-concurrency must be greater than 1" )
61
91
}
62
92
63
- log .Infof ("%s %s starting [loglevel:%s, interval:%s, healthport:%s]" ,
93
+ log .Infof ("%s %s starting [loglevel:%s, interval:%s, healthport:%s]" ,
64
94
version .BinaryName (),
65
95
version .Version (),
66
96
strings .ToUpper (cfg .LogLevel ),
@@ -144,7 +174,7 @@ func newRunCommand() *cobra.Command {
144
174
)
145
175
146
176
// Initialize metrics before starting the metrics server or using any counters
147
- metrics .InitMetrics ()
177
+ metrics .InitMetrics ()
148
178
149
179
// Health server will start in a go routine and run asynchronously
150
180
var hsErrCh chan error
@@ -255,7 +285,7 @@ func newRunCommand() *cobra.Command {
255
285
256
286
// This is our main loop. We leave it only when our health probe server
257
287
// returns an error.
258
- for {
288
+ for {
259
289
select {
260
290
case err := <- hsErrCh :
261
291
if err != nil {
@@ -288,7 +318,7 @@ func newRunCommand() *cobra.Command {
288
318
return nil
289
319
default :
290
320
if lastRun .IsZero () || time .Since (lastRun ) > cfg .CheckInterval {
291
- result , err := runImageUpdater (cfg , false )
321
+ result , err := runImageUpdater (cfg , false )
292
322
if err != nil {
293
323
log .Errorf ("Error: %v" , err )
294
324
} else {
@@ -340,6 +370,9 @@ func newRunCommand() *cobra.Command {
340
370
runCmd .Flags ().StringVar (& cfg .AppLabel , "match-application-label" , "" , "label selector to match application labels against. DEPRECATED: this flag will be removed in a future version." )
341
371
342
372
runCmd .Flags ().BoolVar (& warmUpCache , "warmup-cache" , true , "whether to perform a cache warm-up on startup" )
373
+ runCmd .Flags ().StringVar (& cfg .Schedule , "schedule" , env .GetStringVal ("IMAGE_UPDATER_SCHEDULE" , "default" ), "scheduling policy: default|lru|fail-first" )
374
+ runCmd .Flags ().DurationVar (& cfg .Cooldown , "cooldown" , env .GetDurationVal ("IMAGE_UPDATER_COOLDOWN" , 0 ), "deprioritize apps updated within this duration" )
375
+ runCmd .Flags ().IntVar (& cfg .PerRepoCap , "per-repo-cap" , env .ParseNumFromEnv ("IMAGE_UPDATER_PER_REPO_CAP" , 0 , 0 , 100000 ), "max updates per repo per cycle (0 = unlimited)" )
343
376
runCmd .Flags ().StringVar (& cfg .GitCommitUser , "git-commit-user" , env .GetStringVal ("GIT_COMMIT_USER" , "argocd-image-updater" ), "Username to use for Git commits" )
344
377
runCmd .
Flags ().
StringVar (
& cfg .
GitCommitMail ,
"git-commit-email" ,
env .
GetStringVal (
"GIT_COMMIT_EMAIL" ,
"[email protected] " ),
"E-Mail address to use for Git commits" )
345
378
runCmd .Flags ().StringVar (& cfg .GitCommitSigningKey , "git-commit-signing-key" , env .GetStringVal ("GIT_COMMIT_SIGNING_KEY" , "" ), "GnuPG key ID or path to Private SSH Key used to sign the commits" )
@@ -403,7 +436,7 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
403
436
log .Infof ("Starting image update cycle, considering %d annotated application(s) for update" , len (appList ))
404
437
}
405
438
406
- syncState := argocd .NewSyncIterationState ()
439
+ syncState := argocd .NewSyncIterationState ()
407
440
408
441
// Allow a maximum of MaxConcurrency number of goroutines to exist at the
409
442
// same time. If in warm-up mode, set to 1 explicitly.
@@ -420,7 +453,25 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
420
453
var wg sync.WaitGroup
421
454
wg .Add (len (appList ))
422
455
423
- for app , curApplication := range appList {
456
+ // Optionally reorder apps by scheduling policy
457
+ ordered := make ([]string , 0 , len (appList ))
458
+ for app := range appList { ordered = append (ordered , app ) }
459
+ if cfg .Schedule != "default" || cfg .Cooldown > 0 || cfg .PerRepoCap > 0 {
460
+ ordered = orderApplications (ordered , appList , syncState , cfg )
461
+ }
462
+
463
+ perRepoCounter := map [string ]int {}
464
+
465
+ for _ , app := range ordered {
466
+ curApplication := appList [app ]
467
+ // Per-repo cap if configured
468
+ if cfg .PerRepoCap > 0 {
469
+ repo := argocd .GetApplicationSource (& curApplication .Application ).RepoURL
470
+ if perRepoCounter [repo ] >= cfg .PerRepoCap {
471
+ continue
472
+ }
473
+ }
474
+ syncState .RecordAttempt (app )
424
475
lockErr := sem .Acquire (context .Background (), 1 )
425
476
if lockErr != nil {
426
477
log .Errorf ("Could not acquire semaphore for application %s: %v" , app , lockErr )
@@ -447,7 +498,12 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
447
498
DisableKubeEvents : cfg .DisableKubeEvents ,
448
499
GitCreds : cfg .GitCreds ,
449
500
}
450
- res := argocd .UpdateApplication (upconf , syncState )
501
+ res := argocd .UpdateApplication (upconf , syncState )
502
+ syncState .RecordResult (app , res .NumErrors > 0 )
503
+ if cfg .PerRepoCap > 0 {
504
+ repo := argocd .GetApplicationSource (& curApplication .Application ).RepoURL
505
+ perRepoCounter [repo ] = perRepoCounter [repo ] + 1
506
+ }
451
507
result .NumApplicationsProcessed += 1
452
508
result .NumErrors += res .NumErrors
453
509
result .NumImagesConsidered += res .NumImagesConsidered
0 commit comments