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.
This commit is contained in:
		| @@ -82,7 +82,10 @@ var ( | |||||||
|  |  | ||||||
| 		# Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command | 		# Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command | ||||||
| 		kubectl delete pod/busybox1 | 		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. | // errNoMatchingResources is returned when there is no resources matching a query. | ||||||
| @@ -98,6 +101,7 @@ type WaitFlags struct { | |||||||
|  |  | ||||||
| 	Timeout         time.Duration | 	Timeout         time.Duration | ||||||
| 	ForCondition    string | 	ForCondition    string | ||||||
|  | 	WaitForCreation bool | ||||||
|  |  | ||||||
| 	genericiooptions.IOStreams | 	genericiooptions.IOStreams | ||||||
| } | } | ||||||
| @@ -116,6 +120,7 @@ func NewWaitFlags(restClientGetter genericclioptions.RESTClientGetter, streams g | |||||||
| 			WithLatest(), | 			WithLatest(), | ||||||
|  |  | ||||||
| 		Timeout:         30 * time.Second, | 		Timeout:         30 * time.Second, | ||||||
|  | 		WaitForCreation: true, | ||||||
|  |  | ||||||
| 		IOStreams: streams, | 		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().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().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 | // ToOptions converts from CLI inputs to runtime inputs | ||||||
| @@ -184,6 +190,7 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) { | |||||||
| 		DynamicClient:   dynamicClient, | 		DynamicClient:   dynamicClient, | ||||||
| 		Timeout:         effectiveTimeout, | 		Timeout:         effectiveTimeout, | ||||||
| 		ForCondition:    flags.ForCondition, | 		ForCondition:    flags.ForCondition, | ||||||
|  | 		WaitForCreation: flags.WaitForCreation, | ||||||
|  |  | ||||||
| 		Printer:     printer, | 		Printer:     printer, | ||||||
| 		ConditionFn: conditionFn, | 		ConditionFn: conditionFn, | ||||||
| @@ -306,6 +313,7 @@ type WaitOptions struct { | |||||||
| 	DynamicClient   dynamic.Interface | 	DynamicClient   dynamic.Interface | ||||||
| 	Timeout         time.Duration | 	Timeout         time.Duration | ||||||
| 	ForCondition    string | 	ForCondition    string | ||||||
|  | 	WaitForCreation bool | ||||||
|  |  | ||||||
| 	Printer     printers.ResourcePrinter | 	Printer     printers.ResourcePrinter | ||||||
| 	ConditionFn ConditionFunc | 	ConditionFn ConditionFunc | ||||||
| @@ -320,6 +328,40 @@ func (o *WaitOptions) RunWait() error { | |||||||
| 	ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout) | 	ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout) | ||||||
| 	defer cancel() | 	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 | 	visitCount := 0 | ||||||
| 	visitFunc := func(info *resource.Info, err error) error { | 	visitFunc := func(info *resource.Info, err error) error { | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -338,7 +380,6 @@ func (o *WaitOptions) RunWait() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	visitor := o.ResourceFinder.Do() | 	visitor := o.ResourceFinder.Do() | ||||||
| 	isForDelete := strings.ToLower(o.ForCondition) == "delete" |  | ||||||
| 	if visitor, ok := visitor.(*resource.Result); ok && isForDelete { | 	if visitor, ok := visitor.(*resource.Result); ok && isForDelete { | ||||||
| 		visitor.IgnoreErrors(apierrors.IsNotFound) | 		visitor.IgnoreErrors(apierrors.IsNotFound) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1036,6 +1036,7 @@ runTests() { | |||||||
|   #################### |   #################### | ||||||
|  |  | ||||||
|   record_command run_wait_tests |   record_command run_wait_tests | ||||||
|  |   record_command run_wait_with_non_existence_check_tests | ||||||
|  |  | ||||||
|   #################### |   #################### | ||||||
|   # kubectl debug    # |   # kubectl debug    # | ||||||
|   | |||||||
							
								
								
									
										103
									
								
								test/cmd/wait.sh
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								test/cmd/wait.sh
									
									
									
									
									
								
							| @@ -117,3 +117,106 @@ EOF | |||||||
|     set +o nounset |     set +o nounset | ||||||
|     set +o errexit |     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 - <<EOF | ||||||
|  | apiVersion: apps/v1 | ||||||
|  | kind: Deployment | ||||||
|  | metadata: | ||||||
|  |   labels: | ||||||
|  |     app: dtest | ||||||
|  |   name: dtest | ||||||
|  | spec: | ||||||
|  |   replicas: 3 | ||||||
|  |   selector: | ||||||
|  |     matchLabels: | ||||||
|  |       app: dtest | ||||||
|  |   template: | ||||||
|  |     metadata: | ||||||
|  |       labels: | ||||||
|  |         app: dtest | ||||||
|  |     spec: | ||||||
|  |       containers: | ||||||
|  |       - name: bb | ||||||
|  |         image: busybox | ||||||
|  |         command: ["/bin/sh", "-c", "sleep infinity"] | ||||||
|  | EOF | ||||||
|  |  | ||||||
|  |     set +o errexit | ||||||
|  |     # wait timeout error because condition is invalid | ||||||
|  |     start_sec=$(date +"%s") | ||||||
|  |     output_message=$(time kubectl wait pod --wait-for-creation --selector=app=dtest --for=condition=InvalidCondition --timeout=1s 2>&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 | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Arda Güçlü
					Arda Güçlü