Skip to content

Commit

Permalink
Waiting For a Plan to Finish (#1461)
Browse files Browse the repository at this point in the history
Signed-off-by: Ken Sipe <kensipe@gmail.com>
  • Loading branch information
kensipe committed Apr 14, 2020
1 parent c8e4295 commit bf5c090
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 83 deletions.
2 changes: 2 additions & 0 deletions pkg/kudoctl/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,7 @@ func newInstallCmd(fs afero.Fs) *cobra.Command {
installCmd.Flags().StringVar(&options.OperatorVersion, "operator-version", "", "A specific operator version int the official GitHub repo. (default to the most recent)")
installCmd.Flags().BoolVar(&options.SkipInstance, "skip-instance", false, "If set, install will install the Operator and OperatorVersion, but not an Instance. (default \"false\")")
installCmd.Flags().BoolVar(&options.Wait, "wait", false, "Specify if the CLI should wait for the install to complete before returning (default \"false\")")
installCmd.Flags().Int64Var(&options.WaitTime, "wait-time", 300, "Specify the max wait time in seconds for CLI for the install to complete before returning (default \"300\")")

return installCmd
}
4 changes: 3 additions & 1 deletion pkg/kudoctl/cmd/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package install

import (
"fmt"
"time"

"github.com/spf13/afero"

Expand All @@ -27,6 +28,7 @@ type Options struct {
SkipInstance bool
RequestTimeout int64
Wait bool
WaitTime int64
}

// DefaultOptions initializes the install command options to its defaults
Expand Down Expand Up @@ -76,5 +78,5 @@ func installOperator(operatorArgument string, options *Options, fs afero.Fs, set
return fmt.Errorf("failed to resolve operator package for: %s %w", operatorArgument, err)
}

return kudo.InstallPackage(kc, pkg.Resources, options.SkipInstance, options.InstanceName, settings.Namespace, options.Parameters, options.Wait)
return kudo.InstallPackage(kc, pkg.Resources, options.SkipInstance, options.InstanceName, settings.Namespace, options.Parameters, options.Wait, time.Duration(options.WaitTime)*time.Second)
}
29 changes: 29 additions & 0 deletions pkg/kudoctl/cmd/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const (
`
planStatusExample = ` # View plan status
kubectl kudo plan status --instance=<instanceName>
`
planWaitExample = ` # Wait on the current plan status to finish
kubectl kudo plan wait --instance=<instanceName>
`
planTriggerExample = ` # Trigger an instance plan
kubectl kudo plan trigger <planName> --instance=<instanceName>
Expand All @@ -32,6 +35,7 @@ func newPlanCmd(out io.Writer) *cobra.Command {

cmd.AddCommand(NewPlanHistoryCmd())
cmd.AddCommand(NewPlanStatusCmd(out))
cmd.AddCommand(NewPlanWaitCmd(out))
cmd.AddCommand(NewPlanTriggerCmd())

return cmd
Expand Down Expand Up @@ -67,6 +71,31 @@ func NewPlanStatusCmd(out io.Writer) *cobra.Command {
}

cmd.Flags().StringVar(&options.Instance, "instance", "", "The instance name available from 'kubectl get instances'")
cmd.Flags().BoolVar(&options.Wait, "wait", false, "Specify if the CLI should wait for the plan to complete before returning (default \"false\")")

if err := cmd.MarkFlagRequired("instance"); err != nil {
clog.Printf("Please choose the instance with '--instance=<instanceName>': %v", err)
os.Exit(1)
}

return cmd
}

//NewPlanWaitCmd waits on the status of an instance to complete
func NewPlanWaitCmd(out io.Writer) *cobra.Command {
options := &plan.WaitOptions{Out: out, WaitTime: 300}
cmd := &cobra.Command{
Use: "wait",
Short: "Waits on a plan to finish for a particular instance.",
Example: planWaitExample,
RunE: func(cmd *cobra.Command, args []string) error {
return plan.Wait(options, &Settings)
},
}

cmd.Flags().StringVar(&options.Instance, "instance", "", "The instance name available from 'kubectl get instances'")
cmd.Flags().Int64Var(&options.WaitTime, "wait-time", 300, "Specify the max wait time in seconds for CLI to wait for the current plan to complete (default \"300\")")

if err := cmd.MarkFlagRequired("instance"); err != nil {
clog.Printf("Please choose the instance with '--instance=<instanceName>': %v", err)
os.Exit(1)
Expand Down
1 change: 1 addition & 0 deletions pkg/kudoctl/cmd/plan/plan_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
type Options struct {
Out io.Writer
Instance string
Wait bool
}

var (
Expand Down
171 changes: 111 additions & 60 deletions pkg/kudoctl/cmd/plan/plan_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package plan

import (
"fmt"
"io"
"sort"
"strings"
"time"

"github.com/thoas/go-funk"
"github.com/xlab/treeprint"
Expand All @@ -12,9 +15,6 @@ import (
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
)

// DefaultStatusOptions provides the default options for plan status
var DefaultStatusOptions = &Options{}

// Status runs the plan status command
func Status(options *Options, settings *env.Settings) error {
kc, err := env.GetClient(settings)
Expand All @@ -26,81 +26,132 @@ func Status(options *Options, settings *env.Settings) error {
}

func status(kc *kudo.Client, options *Options, ns string) error {
tree := treeprint.New()

instance, err := kc.GetInstance(options.Instance, ns)
if err != nil {
return err
}
if instance == nil {
return fmt.Errorf("Instance %s/%s does not exist", ns, options.Instance)
}
firstPass := true
start := time.Now()

ov, err := kc.GetOperatorVersion(instance.Spec.OperatorVersion.Name, ns)
if err != nil {
return err
}
if ov == nil {
return fmt.Errorf("OperatorVersion %s from instance %s/%s does not exist", instance.Spec.OperatorVersion.Name, ns, options.Instance)
}
// for loop breaks if Wait==false, or when active plan completes (or when user exits process)
for {
tree := treeprint.New()

lastPlanStatus := instance.GetLastExecutedPlanStatus()
instance, err := kc.GetInstance(options.Instance, ns)
if err != nil {
return err
}
if instance == nil {
return fmt.Errorf("Instance %s/%s does not exist", ns, options.Instance)
}

if lastPlanStatus == nil {
fmt.Fprintf(options.Out, "No plan ever run for instance - nothing to show for instance %s\n", instance.Name)
return nil
}
ov, err := kc.GetOperatorVersion(instance.Spec.OperatorVersion.Name, ns)
if err != nil {
return err
}
if ov == nil {
return fmt.Errorf("OperatorVersion %s from instance %s/%s does not exist", instance.Spec.OperatorVersion.Name, ns, options.Instance)
}

getPhaseStrategy := func(s string) v1beta1.Ordering {
for _, plan := range ov.Spec.Plans {
for _, phase := range plan.Phases {
if phase.Name == s {
return phase.Strategy
lastPlanStatus := instance.GetLastExecutedPlanStatus()

if lastPlanStatus == nil {
fmt.Fprintf(options.Out, "No plan ever run for instance - nothing to show for instance %s\n", instance.Name)
return nil
}

getPhaseStrategy := func(s string) v1beta1.Ordering {
for _, plan := range ov.Spec.Plans {
for _, phase := range plan.Phases {
if phase.Name == s {
return phase.Strategy
}
}
}
return ""
}
return ""
}

rootDisplay := fmt.Sprintf("%s (Operator-Version: \"%s\" Active-Plan: \"%s\")", instance.Name, instance.Spec.OperatorVersion.Name, lastPlanStatus.Name)
rootBranchName := tree.AddBranch(rootDisplay)
rootDisplay := fmt.Sprintf("%s (Operator-Version: \"%s\" Active-Plan: \"%s\")", instance.Name, instance.Spec.OperatorVersion.Name, lastPlanStatus.Name)
rootBranchName := tree.AddBranch(rootDisplay)

plans, _ := funk.Keys(ov.Spec.Plans).([]string)
sort.Strings(plans)
plans, _ := funk.Keys(ov.Spec.Plans).([]string)
sort.Strings(plans)

for _, plan := range plans {
if plan == lastPlanStatus.Name {
planDisplay := fmt.Sprintf("Plan %s (%s strategy) [%s]%s", plan, ov.Spec.Plans[plan].Strategy, lastPlanStatus.Status, printMessageIfAvailable(lastPlanStatus.Message))
if lastPlanStatus.LastUpdatedTimestamp != nil {
planDisplay = fmt.Sprintf("%s, last updated %s", planDisplay, lastPlanStatus.LastUpdatedTimestamp.Format("2006-01-02 15:04:05"))
}
planBranchName := rootBranchName.AddBranch(planDisplay)
for _, phase := range lastPlanStatus.Phases {
phaseDisplay := fmt.Sprintf("Phase %s (%s strategy) [%s]%s", phase.Name, getPhaseStrategy(phase.Name), phase.Status, printMessageIfAvailable(phase.Message))
phaseBranchName := planBranchName.AddBranch(phaseDisplay)
for _, steps := range phase.Steps {
stepsDisplay := fmt.Sprintf("Step %s [%s]%s", steps.Name, steps.Status, printMessageIfAvailable(steps.Message))
phaseBranchName.AddBranch(stepsDisplay)
for _, plan := range plans {
if plan == lastPlanStatus.Name {
planDisplay := fmt.Sprintf("Plan %s (%s strategy) [%s]%s", plan, ov.Spec.Plans[plan].Strategy, lastPlanStatus.Status, printMessageIfAvailable(lastPlanStatus.Message))
if lastPlanStatus.LastUpdatedTimestamp != nil {
planDisplay = fmt.Sprintf("%s, last updated %s", planDisplay, lastPlanStatus.LastUpdatedTimestamp.Format("2006-01-02 15:04:05"))
}
}
} else {
planDisplay := fmt.Sprintf("Plan %s (%s strategy) [NOT ACTIVE]", plan, ov.Spec.Plans[plan].Strategy)
planBranchName := rootBranchName.AddBranch(planDisplay)
for _, phase := range ov.Spec.Plans[plan].Phases {
phaseDisplay := fmt.Sprintf("Phase %s (%s strategy) [NOT ACTIVE]", phase.Name, phase.Strategy)
phaseBranchName := planBranchName.AddBranch(phaseDisplay)
for _, steps := range phase.Steps {
stepDisplay := fmt.Sprintf("Step %s [NOT ACTIVE]", steps.Name)
phaseBranchName.AddBranch(stepDisplay)
planBranchName := rootBranchName.AddBranch(planDisplay)
for _, phase := range lastPlanStatus.Phases {
phaseDisplay := fmt.Sprintf("Phase %s (%s strategy) [%s]%s", phase.Name, getPhaseStrategy(phase.Name), phase.Status, printMessageIfAvailable(phase.Message))
phaseBranchName := planBranchName.AddBranch(phaseDisplay)
for _, steps := range phase.Steps {
stepsDisplay := fmt.Sprintf("Step %s [%s]%s", steps.Name, steps.Status, printMessageIfAvailable(steps.Message))
phaseBranchName.AddBranch(stepsDisplay)
}
}
} else {
planDisplay := fmt.Sprintf("Plan %s (%s strategy) [NOT ACTIVE]", plan, ov.Spec.Plans[plan].Strategy)
planBranchName := rootBranchName.AddBranch(planDisplay)
for _, phase := range ov.Spec.Plans[plan].Phases {
phaseDisplay := fmt.Sprintf("Phase %s (%s strategy) [NOT ACTIVE]", phase.Name, phase.Strategy)
phaseBranchName := planBranchName.AddBranch(phaseDisplay)
for _, steps := range phase.Steps {
stepDisplay := fmt.Sprintf("Step %s [NOT ACTIVE]", steps.Name)
phaseBranchName.AddBranch(stepDisplay)
}
}
}
}
// exec on first go, otherwise don't
if firstPass {
fmt.Fprintf(options.Out, "Plan(s) for \"%s\" in namespace \"%s\":\n", instance.Name, ns)
}
// exec on all loop passes except the first
if !firstPass {
height := strings.Count(tree.String(), "\n") + 1
clearLines(options.Out, height)
}
fmt.Fprintln(options.Out, tree.String())
firstPass = false
if options.Wait {
elapsed := time.Since(start)
clearLine(options.Out)
fmt.Fprintf(options.Out, "elapsed time %s", elapsed)
} else {
break
}
done, err := kc.IsInstanceDone(instance, nil)
if err != nil {
return err
}
if done {
break
}
// freq of updates
time.Sleep(1 * time.Second)
}
return nil
}

// moves terminal cursor up number of lines specified
func moveCursorUp(w io.Writer, lines int) {
fmt.Fprintf(w, "\033[%dA", lines)
}

fmt.Fprintf(options.Out, "Plan(s) for \"%s\" in namespace \"%s\":\n", instance.Name, ns)
fmt.Fprintln(options.Out, tree.String())
// clears the current terminal line
func clearLine(w io.Writer) {
fmt.Fprint(w, "\u001b[0K\r")
}

return nil
// clears multiple terminal lines from current position up to defined height
// useful to clear previous terminal output in order to rewrite to that screen section
func clearLines(w io.Writer, height int) {
moveCursorUp(w, height)
for i := 0; i < height; i++ {
clearLine(w)
fmt.Fprint(w, "\n")
}
moveCursorUp(w, height)
}

func printMessageIfAvailable(s string) string {
Expand Down
56 changes: 56 additions & 0 deletions pkg/kudoctl/cmd/plan/plan_wait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package plan

import (
"errors"
"fmt"
"io"
"time"

pollwait "k8s.io/apimachinery/pkg/util/wait"

"github.com/kudobuilder/kudo/pkg/kudoctl/env"
"github.com/kudobuilder/kudo/pkg/kudoctl/util/kudo"
)

// Options are the configurable options for plans
type WaitOptions struct {
Out io.Writer
Instance string
WaitTime int64
}

// Status runs the plan status command
func Wait(options *WaitOptions, settings *env.Settings) error {
kc, err := env.GetClient(settings)
if err != nil {
return err
}
//return status(kc, options, settings.Namespace)
return wait(kc, options, settings.Namespace)
}

func wait(kc *kudo.Client, options *WaitOptions, ns string) error {
instance, err := kc.GetInstance(options.Instance, ns)
if err != nil {
return err
}
if instance == nil {
return fmt.Errorf("instance %s/%s does not exist", ns, options.Instance)
}

planStatus := instance.GetLastExecutedPlanStatus()
if planStatus == nil {
return fmt.Errorf("instance %s/%s does not have an active plan", ns, options.Instance)
}

fmt.Fprintf(options.Out, "waiting on instance %s/%s with plan %q\n", ns, options.Instance, planStatus.Name)
err = kc.WaitForInstance(options.Instance, ns, nil, time.Duration(options.WaitTime)*time.Second)
if errors.Is(err, pollwait.ErrWaitTimeout) {
_, _ = fmt.Fprintf(options.Out, "timeout waiting for instance %s/%s on plan %q\n", ns, options.Instance, planStatus.Name)
}
if err != nil {
return err
}
_, _ = fmt.Fprintf(options.Out, "instance %s/%s plan %q finished\n", ns, options.Instance, planStatus.Name)
return nil
}
Loading

0 comments on commit bf5c090

Please sign in to comment.