Skip to content

Commit

Permalink
Kubectl: Implement support for multiple --for statements in wait
Browse files Browse the repository at this point in the history
Implement support for multiple --for statements in wait. By default,
multiple --for conditions are OR'ed. With the optional --for-all
flag, they can be AND'ed.

Fixes: kubernetes/kubectl#1224
Reported-at: kubernetes/kubectl#1224
Signed-off-by: Andreas Karis <ak.karis@gmail.com>
  • Loading branch information
andreaskaris committed Jun 21, 2023
1 parent 247ab2b commit cf3b640
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 33 deletions.
6 changes: 3 additions & 3 deletions staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,10 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
UIDMap: uidMap,
DynamicClient: o.DynamicClient,
Timeout: effectiveTimeout,
ForConditions: map[string]cmdwait.ConditionFunc{"delete": cmdwait.IsDeleted},

Printer: printers.NewDiscardingPrinter(),
ConditionFn: cmdwait.IsDeleted,
IOStreams: o.IOStreams,
Printer: printers.NewDiscardingPrinter(),
IOStreams: o.IOStreams,
}
err = waitOptions.RunWait()
if errors.IsForbidden(err) || errors.IsMethodNotSupported(err) {
Expand Down
69 changes: 52 additions & 17 deletions staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/genericclioptions"
Expand Down Expand Up @@ -91,7 +92,8 @@ type WaitFlags struct {
ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags

Timeout time.Duration
ForCondition string
ForCondition []string
ForAll bool

genericiooptions.IOStreams
}
Expand Down Expand Up @@ -120,7 +122,7 @@ func NewCmdWait(restClientGetter genericclioptions.RESTClientGetter, streams gen
flags := NewWaitFlags(restClientGetter, streams)

cmd := &cobra.Command{
Use: "wait ([-f FILENAME] | resource.group/resource.name | resource.group [(-l label | --all)]) [--for=delete|--for condition=available|--for=jsonpath='{}'=value]",
Use: "wait ([-f FILENAME] | resource.group/resource.name | resource.group [(-l label | --all)]) [--for-all] {--for=delete|--for condition=available|--for=jsonpath='{}'=value}",
Short: i18n.T("Experimental: Wait for a specific condition on one or many resources"),
Long: waitLong,
Example: waitExample,
Expand All @@ -145,7 +147,9 @@ func (flags *WaitFlags) AddFlags(cmd *cobra.Command) {
flags.ResourceBuilderFlags.AddFlags(cmd.Flags())

cmd.Flags().DurationVar(&flags.Timeout, "timeout", flags.Timeout, "The length of time to wait before giving up. Zero means check once and don't wait, negative means wait for a week.")
cmd.Flags().StringVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [delete|condition=condition-name[=condition-value]|jsonpath='{JSONPath expression}'=JSONPath Condition]. The default condition-value is true. Condition values are compared after Unicode simple case folding, which is a more general form of case-insensitivity.")
cmd.Flags().StringArrayVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [delete|condition=condition-name[=condition-value]|jsonpath='{JSONPath expression}'=JSONPath Condition]. The default condition-value is true. Condition values are compared after Unicode simple case folding, which is a more general form of case-insensitivity. This option may be repeated to wait for one (default) or all of several conditions conditions.")
cmd.Flags().BoolVar(&flags.ForAll, "for-all", flags.ForAll, "Wait for all conditions to be true when multiple "+
"for flags are provided")
}

// ToOptions converts from CLI inputs to runtime inputs
Expand All @@ -163,9 +167,14 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) {
if err != nil {
return nil, err
}
conditionFn, err := conditionFuncFor(flags.ForCondition, flags.ErrOut)
if err != nil {
return nil, err

forConditions := make(map[string]ConditionFunc)
for _, condition := range flags.ForCondition {
conditionFn, err := conditionFuncFor(condition, flags.ErrOut)
if err != nil {
return nil, err
}
forConditions[condition] = conditionFn
}

effectiveTimeout := flags.Timeout
Expand All @@ -177,11 +186,11 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) {
ResourceFinder: builder,
DynamicClient: dynamicClient,
Timeout: effectiveTimeout,
ForCondition: flags.ForCondition,
ForConditions: forConditions,
ForAll: flags.ForAll,

Printer: printer,
ConditionFn: conditionFn,
IOStreams: flags.IOStreams,
Printer: printer,
IOStreams: flags.IOStreams,
}

return o, nil
Expand Down Expand Up @@ -275,29 +284,55 @@ type WaitOptions struct {
UIDMap UIDMap
DynamicClient dynamic.Interface
Timeout time.Duration
ForCondition string
ForConditions map[string]ConditionFunc
ForAll bool

Printer printers.ResourcePrinter
ConditionFn ConditionFunc
Printer printers.ResourcePrinter
genericiooptions.IOStreams
}

// ConditionFunc is the interface for providing condition checks
type ConditionFunc func(ctx context.Context, info *resource.Info, o *WaitOptions) (finalObject runtime.Object, done bool, err error)

// RunWait runs the waiting logic
// RunWait runs the waiting logic.
func (o *WaitOptions) RunWait() error {
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
defer cancel()

if len(o.ForConditions) == 0 {
return errors.New("provide at least one for condition to wait on")
}

errChan := make(chan error, len(o.ForConditions))
for condition := range o.ForConditions {
condition := condition
go func(errChan chan<- error) {
errChan <- o.runWaitCondition(ctx, condition)
}(errChan)
}
var errors []error
for range o.ForConditions {
if err := <-errChan; err != nil {
if o.ForAll {
return err
}
errors = append(errors, err)
} else if !o.ForAll {
return nil
}
}
return utilerrors.NewAggregate(errors)
}

// runWaitCondition runs the waiting logic for a single provided condition.
func (o *WaitOptions) runWaitCondition(ctx context.Context, condition string) error {
visitCount := 0
visitFunc := func(info *resource.Info, err error) error {
if err != nil {
return err
}

visitCount++
finalObject, success, err := o.ConditionFn(ctx, info, o)
finalObject, success, err := o.ForConditions[condition](ctx, info, o)
if success {
o.Printer.PrintObj(finalObject, o.Out)
return nil
Expand All @@ -308,7 +343,7 @@ func (o *WaitOptions) RunWait() error {
return err
}
visitor := o.ResourceFinder.Do()
isForDelete := strings.ToLower(o.ForCondition) == "delete"
isForDelete := strings.ToLower(condition) == "delete"
if visitor, ok := visitor.(*resource.Result); ok && isForDelete {
visitor.IgnoreErrors(apierrors.IsNotFound)
}
Expand Down

0 comments on commit cf3b640

Please sign in to comment.