From e24b9a022f3b1e97ea538c9754d4d38f119f275e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 26 Jan 2024 15:53:55 +0300 Subject: [PATCH] Add new --wait-for-creation flag in kubectl wait command kubectl wait command errors out when the waited resource does not exist. But we need to provide a way to the users about intentionally also waiting for the creation of resources. This PR introduces a new flag to cover waiting for the creation of resources with preserving the default behavior. --- .../src/k8s.io/kubectl/pkg/cmd/wait/wait.go | 67 +++++++++--- test/cmd/legacy-script.sh | 1 + test/cmd/wait.sh | 103 ++++++++++++++++++ 3 files changed, 158 insertions(+), 13 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go b/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go index e211ccec6cd..f453d4ba62f 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go @@ -82,7 +82,10 @@ var ( # Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command kubectl delete pod/busybox1 - kubectl wait --for=delete pod/busybox1 --timeout=60s`)) + kubectl wait --for=delete pod/busybox1 --timeout=60s + + # Wait for the creation of the service "loadbalancer" in addition to wait to have ingress + kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer --wait-for-creation`)) ) // errNoMatchingResources is returned when there is no resources matching a query. @@ -96,8 +99,9 @@ type WaitFlags struct { PrintFlags *genericclioptions.PrintFlags ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags - Timeout time.Duration - ForCondition string + Timeout time.Duration + ForCondition string + WaitForCreation bool genericiooptions.IOStreams } @@ -115,7 +119,8 @@ func NewWaitFlags(restClientGetter genericclioptions.RESTClientGetter, streams g WithLocal(false). WithLatest(), - Timeout: 30 * time.Second, + Timeout: 30 * time.Second, + WaitForCreation: true, IOStreams: streams, } @@ -152,6 +157,7 @@ func (flags *WaitFlags) AddFlags(cmd *cobra.Command) { 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 value]]. 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().BoolVar(&flags.WaitForCreation, "wait-for-creation", flags.WaitForCreation, "The default value is true. If set to true, also wait for creation of objects if they do not already exist. This flag is ignored in --for=delete") } // ToOptions converts from CLI inputs to runtime inputs @@ -180,10 +186,11 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) { } o := &WaitOptions{ - ResourceFinder: builder, - DynamicClient: dynamicClient, - Timeout: effectiveTimeout, - ForCondition: flags.ForCondition, + ResourceFinder: builder, + DynamicClient: dynamicClient, + Timeout: effectiveTimeout, + ForCondition: flags.ForCondition, + WaitForCreation: flags.WaitForCreation, Printer: printer, ConditionFn: conditionFn, @@ -302,10 +309,11 @@ type WaitOptions struct { ResourceFinder genericclioptions.ResourceFinder // UIDMap maps a resource location to a UID. It is optional, but ConditionFuncs may choose to use it to make the result // more reliable. For instance, delete can look for UID consistency during delegated calls. - UIDMap UIDMap - DynamicClient dynamic.Interface - Timeout time.Duration - ForCondition string + UIDMap UIDMap + DynamicClient dynamic.Interface + Timeout time.Duration + ForCondition string + WaitForCreation bool Printer printers.ResourcePrinter ConditionFn ConditionFunc @@ -320,6 +328,40 @@ func (o *WaitOptions) RunWait() error { ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout) defer cancel() + isForDelete := strings.ToLower(o.ForCondition) == "delete" + if o.WaitForCreation && o.Timeout == 0 { + return fmt.Errorf("--wait-for-creation requires a timeout value greater than 0") + } + + if o.WaitForCreation && !isForDelete { + err := func() error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("context deadline is exceeded while waiting for the creation of the resources") + default: + err := o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error { + // We don't need to do anything after we assure that the resources exist. Because + // actual logic will be incorporated after we wait all the resources' existence. + return nil + }) + // It is verified that all the resources exist. + if err == nil { + return nil + } + // We specifically wait for the creation of resources and all the errors + // other than not found means that this is something we cannot handle. + if !apierrors.IsNotFound(err) { + return err + } + } + } + }() + if err != nil { + return err + } + } + visitCount := 0 visitFunc := func(info *resource.Info, err error) error { if err != nil { @@ -338,7 +380,6 @@ func (o *WaitOptions) RunWait() error { return err } visitor := o.ResourceFinder.Do() - isForDelete := strings.ToLower(o.ForCondition) == "delete" if visitor, ok := visitor.(*resource.Result); ok && isForDelete { visitor.IgnoreErrors(apierrors.IsNotFound) } diff --git a/test/cmd/legacy-script.sh b/test/cmd/legacy-script.sh index 10ab751f14b..bb7006acc1d 100755 --- a/test/cmd/legacy-script.sh +++ b/test/cmd/legacy-script.sh @@ -1036,6 +1036,7 @@ runTests() { #################### record_command run_wait_tests + record_command run_wait_with_non_existence_check_tests #################### # kubectl debug # diff --git a/test/cmd/wait.sh b/test/cmd/wait.sh index d8bd7b17226..a5f81c8e772 100644 --- a/test/cmd/wait.sh +++ b/test/cmd/wait.sh @@ -117,3 +117,106 @@ EOF set +o nounset set +o errexit } + +run_wait_with_non_existence_check_tests() { + set -o nounset + set -o errexit + + kube::log::status "Testing kubectl wait" + + create_and_use_new_namespace + + ### Wait for deletion using --all flag + + # create test data + kubectl create deployment test-1 --image=busybox + kubectl create deployment test-2 --image=busybox + + # Post-Condition: deployments exists + kube::test::get_object_assert "deployments" "{{range .items}}{{.metadata.name}},{{end}}" 'test-1,test-2,' + + # wait with jsonpath will timout for busybox deployment + set +o errexit + # Command: Wait with jsonpath support fields not exist in the first place + output_message=$(kubectl wait --wait-for-creation --for=jsonpath=.status.readyReplicas=1 deploy/test-1 2>&1) + set -o errexit + + # Post-Condition: Wait failed + kube::test::if_has_string "${output_message}" 'timed out' + + # Delete all deployments async to kubectl wait + ( sleep 2 && kubectl delete deployment --all ) & + + # Command: Wait for all deployments to be deleted + output_message=$(kubectl wait deployment --for=delete --all) + + # Post-Condition: Wait was successful + kube::test::if_has_string "${output_message}" 'test-1 condition met' + kube::test::if_has_string "${output_message}" 'test-2 condition met' + + # create test data to test timeout error is occurred in correct time + kubectl apply -f - <&1) + end_sec=$(date +"%s") + len_sec=$((end_sec-start_sec)) + set -o errexit + kube::test::if_has_string "${output_message}" 'timed out waiting for the condition ' + test $len_sec -ge 1 && test $len_sec -le 2 + + # Clean deployment + kubectl delete deployment dtest + + # create test data + kubectl create deployment test-3 --image=busybox + + # wait with jsonpath without value to succeed + set +o errexit + # Command: Wait with jsonpath without value + output_message_0=$(kubectl wait --wait-for-creation --for=jsonpath='{.status.replicas}' deploy/test-3 2>&1) + # Command: Wait with relaxed jsonpath and filter expression + output_message_1=$(kubectl wait \ + --for='jsonpath=spec.template.spec.containers[?(@.name=="busybox")].image=busybox' \ + deploy/test-3) + set -o errexit + + # Post-Condition: Wait succeed + kube::test::if_has_string "${output_message_0}" 'deployment.apps/test-3 condition met' + kube::test::if_has_string "${output_message_1}" 'deployment.apps/test-3 condition met' + + # Clean deployment + kubectl delete deployment test-3 + + ( sleep 3 && kubectl create deployment test-4 --image=busybox ) & + output_message=$(kubectl wait --wait-for-creation --for=jsonpath=.status.replicas=1 deploy/test-4 2>&1) + kube::test::if_has_string "${output_message}" 'test-4 condition met' + + kubectl delete deployment test-4 + + set +o nounset + set +o errexit +}