Skip to content

Commit

Permalink
Add argocd app wait command (#216)
Browse files Browse the repository at this point in the history
* Update CLI, server for wait request

* Update generated code

* Remove generated code

* Add timeout function, and use it

* Get first working prototype

* Properly fail and print success/fail messages

* Add missing reference pointer

* Remove unreachable code

* Show current state of all checks

* Print atypical health output status now

* Update short command description, thanks @jessesuen

* Use server-side watch command

* Use watch API

* Clean up wait function to use new API better

* Rm unused const, satisfy linter on caps names

* Rename channel and set direction

* Add infinite timeout by default
  • Loading branch information
merenbach authored May 18, 2018
1 parent afd5450 commit ac0f623
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 0 deletions.
73 changes: 73 additions & 0 deletions cmd/argocd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
command.AddCommand(NewApplicationRollbackCommand(clientOpts))
command.AddCommand(NewApplicationListCommand(clientOpts))
command.AddCommand(NewApplicationDeleteCommand(clientOpts))
command.AddCommand(NewApplicationWaitCommand(clientOpts))
return command
}

Expand Down Expand Up @@ -336,6 +337,78 @@ func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
return command
}

// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
syncOnly bool
healthOnly bool
timeout uint
)
const defaultCheckTimeoutSeconds = 0
var command = &cobra.Command{
Use: "wait APPNAME",
Short: "Wait for an application to reach a synced and healthy state",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if syncOnly && healthOnly {
log.Fatalln("Please specify at most one of --sync-only or --health-only.")
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)

appName := args[0]
wc, err := appIf.Watch(context.Background(), &application.ApplicationQuery{
Name: &appName,
})
errors.CheckError(err)

success := util.Wait(timeout, func(done chan<- bool) {
for {
appEvent, err := wc.Recv()
errors.CheckError(err)

app := appEvent.Application
healthStatus := app.Status.Health.Status
syncStatus := app.Status.ComparisonResult.Status

log.Printf("App %q has sync status %q and health status %q", appName, syncStatus, healthStatus)
synced := (syncStatus == argoappv1.ComparisonStatusSynced)
healthy := (healthStatus == argoappv1.HealthStatusHealthy)

if (synced && healthy) || (synced && syncOnly) || (healthy && healthOnly) {
done <- true
}
}
})

if success {
log.Printf("App %q matches desired state", appName)
} else {
app, err := appIf.Get(context.Background(), &application.ApplicationQuery{Name: &appName})
errors.CheckError(err)

log.Errorf("Timed out before seeing app %q match desired state", appName)
if len(app.Status.ComparisonResult.Resources) > 0 {
for _, res := range app.Status.ComparisonResult.Resources {
targetObj, err := argoappv1.UnmarshalToUnstructured(res.TargetState)
errors.CheckError(err)
if res.Status != argoappv1.ComparisonStatusSynced || res.Health.Status != argoappv1.HealthStatusHealthy {
log.Warnf("%s %q has sync status %q and health status %q: %s", targetObj.GetKind(), targetObj.GetName(), res.Status, res.Health.Status, res.Health.StatusDetails)
}
}
}
}
},
}
command.Flags().BoolVar(&syncOnly, "sync-only", false, "Wait only for sync")
command.Flags().BoolVar(&healthOnly, "health-only", false, "Wait only for health")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
return command
}

// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
Expand Down
28 changes: 28 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package util

import (
"time"
)

type Closer interface {
Close() error
}
Expand All @@ -9,3 +13,27 @@ type Closer interface {
func Close(c Closer) {
_ = c.Close()
}

// Wait takes a check interval and timeout and waits for a function to return `true`.
// Wait will return `true` on success and `false` on timeout.
// The passed function, in turn, should pass `true` (or anything, really) to the channel when it's done.
// Pass `0` as the timeout to run infinitely until completion.
func Wait(timeout uint, f func(chan<- bool)) bool {
done := make(chan bool)
go f(done)

// infinite
if timeout == 0 {
return <-done
}

timedOut := time.After(time.Duration(timeout) * time.Second)
for {
select {
case <-done:
return true
case <-timedOut:
return false
}
}
}

0 comments on commit ac0f623

Please sign in to comment.