@@ -12,6 +12,7 @@ import (
1212 "sort"
1313 "strconv"
1414 "strings"
15+ "sync"
1516 "text/tabwriter"
1617 "time"
1718 "unicode/utf8"
@@ -301,8 +302,8 @@ func parentChildDetails(ctx context.Context, appIf application.ApplicationServic
301302}
302303
303304func printHeader (ctx context.Context , acdClient argocdclient.Client , app * argoappv1.Application , windows * argoappv1.SyncWindows , showOperation bool , showParams bool , sourcePosition int ) {
304- aURL := appURL (ctx , acdClient , app .Name )
305- printAppSummaryTable (app , aURL , windows )
305+ appURL := getAppURL (ctx , acdClient , app .Name )
306+ printAppSummaryTable (app , appURL , windows )
306307
307308 if len (app .Status .Conditions ) > 0 {
308309 fmt .Println ()
@@ -337,6 +338,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
337338 refresh bool
338339 hardRefresh bool
339340 output string
341+ timeout uint
340342 showParams bool
341343 showOperation bool
342344 appNamespace string
@@ -382,7 +384,8 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
382384 ` ),
383385
384386 Run : func (c * cobra.Command , args []string ) {
385- ctx := c .Context ()
387+ ctx , cancel := context .WithCancel (c .Context ())
388+ defer cancel ()
386389 if len (args ) == 0 {
387390 c .HelpFunc ()(c , args )
388391 os .Exit (1 )
@@ -393,13 +396,54 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
393396
394397 appName , appNs := argo .ParseFromQualifiedName (args [0 ], appNamespace )
395398
396- app , err := appIf .Get (ctx , & application.ApplicationQuery {
397- Name : & appName ,
398- Refresh : getRefreshType (refresh , hardRefresh ),
399- AppNamespace : & appNs ,
400- })
399+ if timeout != 0 {
400+ time .AfterFunc (time .Duration (timeout )* time .Second , func () {
401+ if ctx .Err () != nil {
402+ fmt .Println ("Timeout function: context already cancelled:" , ctx .Err ())
403+ } else {
404+ fmt .Println ("Timeout function: cancelling context manually" )
405+ cancel ()
406+ }
407+ })
408+ }
409+ getAppStateWithRetry := func () (* argoappv1.Application , error ) {
410+ type getResponse struct {
411+ app * argoappv1.Application
412+ err error
413+ }
414+
415+ ch := make (chan getResponse , 1 )
416+
417+ go func () {
418+ app , err := appIf .Get (ctx , & application.ApplicationQuery {
419+ Name : & appName ,
420+ Refresh : getRefreshType (refresh , hardRefresh ),
421+ AppNamespace : & appNs ,
422+ })
423+ ch <- getResponse {app : app , err : err }
424+ }()
425+
426+ select {
427+ case result := <- ch :
428+ return result .app , result .err
429+ case <- ctx .Done ():
430+ // Timeout occurred, try again without refresh flag
431+ // Create new context for retry request
432+ ctx := context .Background ()
433+ app , err := appIf .Get (ctx , & application.ApplicationQuery {
434+ Name : & appName ,
435+ AppNamespace : & appNs ,
436+ })
437+ return app , err
438+ }
439+ }
440+
441+ app , err := getAppStateWithRetry ()
401442 errors .CheckError (err )
402443
444+ if ctx .Err () != nil {
445+ ctx = context .Background () // Reset context for subsequent requests
446+ }
403447 if sourceName != "" && sourcePosition != - 1 {
404448 errors .Fatal (errors .ErrorGeneric , "Only one of source-position and source-name can be specified." )
405449 }
@@ -462,6 +506,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
462506 },
463507 }
464508 command .Flags ().StringVarP (& output , "output" , "o" , "wide" , "Output format. One of: json|yaml|wide|tree" )
509+ command .Flags ().UintVar (& timeout , "timeout" , defaultCheckTimeoutSeconds , "Time out after this many seconds" )
465510 command .Flags ().BoolVar (& showOperation , "show-operation" , false , "Show application operation" )
466511 command .Flags ().BoolVar (& showParams , "show-params" , false , "Show application parameters and overrides" )
467512 command .Flags ().BoolVar (& refresh , "refresh" , false , "Refresh application data when retrieving" )
@@ -731,8 +776,8 @@ func appURLDefault(acdClient argocdclient.Client, appName string) string {
731776 return fmt .Sprintf ("%s://%s/applications/%s" , scheme , server , appName )
732777}
733778
734- // appURL returns the URL of an application
735- func appURL (ctx context.Context , acdClient argocdclient.Client , appName string ) string {
779+ // getAppURL returns the URL of an application
780+ func getAppURL (ctx context.Context , acdClient argocdclient.Client , appName string ) string {
736781 conn , settingsIf := acdClient .NewSettingsClientOrDie ()
737782 defer argoio .Close (conn )
738783 argoSettings , err := settingsIf .Get (ctx , & settings.SettingsQuery {})
@@ -2518,18 +2563,47 @@ func resourceParentChild(ctx context.Context, acdClient argocdclient.Client, app
25182563
25192564const waitFormatString = "%s\t %5s\t %10s\t %10s\t %20s\t %8s\t %7s\t %10s\t %s\n "
25202565
2566+ // AppWithLock encapsulates the application and its lock
2567+ type AppWithLock struct {
2568+ mu sync.Mutex
2569+ app * argoappv1.Application
2570+ }
2571+
2572+ // NewAppWithLock creates a new AppWithLock instance
2573+ func NewAppWithLock () * AppWithLock {
2574+ return & AppWithLock {}
2575+ }
2576+
2577+ // SetApp safely updates the application
2578+ func (a * AppWithLock ) SetApp (app * argoappv1.Application ) {
2579+ a .mu .Lock ()
2580+ defer a .mu .Unlock ()
2581+ a .app = app
2582+ }
2583+
2584+ // GetApp safely retrieves the application
2585+ func (a * AppWithLock ) GetApp () * argoappv1.Application {
2586+ a .mu .Lock ()
2587+ defer a .mu .Unlock ()
2588+ return a .app
2589+ }
2590+
25212591// waitOnApplicationStatus watches an application and blocks until either the desired watch conditions
25222592// are fulfilled or we reach the timeout. Returns the app once desired conditions have been filled.
25232593// Additionally return the operationState at time of fulfilment (which may be different than returned app).
25242594func waitOnApplicationStatus (ctx context.Context , acdClient argocdclient.Client , appName string , timeout uint , watch watchOpts , selectedResources []* argoappv1.SyncOperationResource , output string ) (* argoappv1.Application , * argoappv1.OperationState , error ) {
25252595 ctx , cancel := context .WithCancel (ctx )
25262596 defer cancel ()
25272597
2598+ appWithLock := NewAppWithLock ()
25282599 // refresh controls whether or not we refresh the app before printing the final status.
25292600 // We only want to do this when an operation is in progress, since operations are the only
25302601 // time when the sync status lags behind when an operation completes
25312602 refresh := false
25322603
2604+ // appURL is declared here so that it can be used in the printFinalStatus function when the context is cancelled
2605+ appURL := getAppURL (ctx , acdClient , appName )
2606+
25332607 // printSummary controls whether we print the app summary table, OperationState, and ResourceState
25342608 // We don't want to print these when output type is json or yaml, as the output would become unparsable.
25352609 printSummary := output != "json" && output != "yaml"
@@ -2552,7 +2626,7 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client,
25522626
25532627 if printSummary {
25542628 fmt .Println ()
2555- printAppSummaryTable (app , appURL ( ctx , acdClient , appName ) , nil )
2629+ printAppSummaryTable (app , appURL , nil )
25562630 fmt .Println ()
25572631 if watch .operation {
25582632 printOperationResult (app .Status .OperationState )
@@ -2591,19 +2665,24 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client,
25912665
25922666 if timeout != 0 {
25932667 time .AfterFunc (time .Duration (timeout )* time .Second , func () {
2594- _ , appClient := acdClient .NewApplicationClientOrDie ()
2668+ conn , appClient := acdClient .NewApplicationClientOrDie ()
2669+ defer conn .Close ()
2670+ // We want to print the final status of the app even if the conditions are not met
2671+ if printSummary {
2672+ fmt .Println ()
2673+ fmt .Println ("This is the state of the app after wait timed out:" )
2674+ }
2675+ // Setting refresh = false because we don't want printFinalStatus to execute a refresh
2676+ refresh = false
2677+ // Updating the app object to the latest state
25952678 app , err := appClient .Get (ctx , & application.ApplicationQuery {
25962679 Name : & appRealName ,
25972680 AppNamespace : & appNs ,
25982681 })
25992682 errors .CheckError (err )
2600-
2601- if printSummary {
2602- fmt .Println ()
2603- fmt .Println ("This is the state of the app after `wait` timed out:" )
2604- }
2605-
2606- printFinalStatus (app )
2683+ // Update the app object
2684+ appWithLock .SetApp (app )
2685+ // Cancel the context to stop the watch
26072686 cancel ()
26082687
26092688 if printSummary {
@@ -2626,18 +2705,20 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client,
26262705 AppNamespace : & appNs ,
26272706 })
26282707 errors .CheckError (err )
2708+ appWithLock .SetApp (app ) // Update the app object
26292709
26302710 // printFinalStatus() will refresh and update the app object, potentially causing the app's
26312711 // status.operationState to be different than the version when we break out of the event loop.
26322712 // This means the app.status is unreliable for determining the final state of the operation.
26332713 // finalOperationState captures the operationState as it was seen when we met the conditions of
26342714 // the wait, so the caller can rely on it to determine the outcome of the operation.
26352715 // See: https://github.com/argoproj/argo-cd/issues/5592
2636- finalOperationState := app .Status .OperationState
2716+ finalOperationState := appWithLock . GetApp () .Status .OperationState
26372717
2638- appEventCh := acdClient .WatchApplicationWithRetry (ctx , appName , app .ResourceVersion )
2718+ appEventCh := acdClient .WatchApplicationWithRetry (ctx , appName , appWithLock . GetApp () .ResourceVersion )
26392719 for appEvent := range appEventCh {
2640- app = & appEvent .Application
2720+ appWithLock .SetApp (& appEvent .Application )
2721+ app = appWithLock .GetApp ()
26412722
26422723 finalOperationState = app .Status .OperationState
26432724 operationInProgress := false
@@ -2708,7 +2789,7 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client,
27082789 }
27092790 _ = w .Flush ()
27102791 }
2711- _ = printFinalStatus (app )
2792+ _ = printFinalStatus (appWithLock . GetApp () )
27122793 return nil , finalOperationState , fmt .Errorf ("timed out (%ds) waiting for app %q match desired state" , timeout , appName )
27132794}
27142795
0 commit comments