Skip to content

Commit e24b9a0

Browse files
committed
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.
1 parent 8565e37 commit e24b9a0

File tree

3 files changed

+158
-13
lines changed

3 files changed

+158
-13
lines changed

staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ var (
8282
8383
# Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command
8484
kubectl delete pod/busybox1
85-
kubectl wait --for=delete pod/busybox1 --timeout=60s`))
85+
kubectl wait --for=delete pod/busybox1 --timeout=60s
86+
87+
# Wait for the creation of the service "loadbalancer" in addition to wait to have ingress
88+
kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer --wait-for-creation`))
8689
)
8790

8891
// errNoMatchingResources is returned when there is no resources matching a query.
@@ -96,8 +99,9 @@ type WaitFlags struct {
9699
PrintFlags *genericclioptions.PrintFlags
97100
ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags
98101

99-
Timeout time.Duration
100-
ForCondition string
102+
Timeout time.Duration
103+
ForCondition string
104+
WaitForCreation bool
101105

102106
genericiooptions.IOStreams
103107
}
@@ -115,7 +119,8 @@ func NewWaitFlags(restClientGetter genericclioptions.RESTClientGetter, streams g
115119
WithLocal(false).
116120
WithLatest(),
117121

118-
Timeout: 30 * time.Second,
122+
Timeout: 30 * time.Second,
123+
WaitForCreation: true,
119124

120125
IOStreams: streams,
121126
}
@@ -152,6 +157,7 @@ func (flags *WaitFlags) AddFlags(cmd *cobra.Command) {
152157

153158
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.")
154159
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.")
160+
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")
155161
}
156162

157163
// ToOptions converts from CLI inputs to runtime inputs
@@ -180,10 +186,11 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) {
180186
}
181187

182188
o := &WaitOptions{
183-
ResourceFinder: builder,
184-
DynamicClient: dynamicClient,
185-
Timeout: effectiveTimeout,
186-
ForCondition: flags.ForCondition,
189+
ResourceFinder: builder,
190+
DynamicClient: dynamicClient,
191+
Timeout: effectiveTimeout,
192+
ForCondition: flags.ForCondition,
193+
WaitForCreation: flags.WaitForCreation,
187194

188195
Printer: printer,
189196
ConditionFn: conditionFn,
@@ -302,10 +309,11 @@ type WaitOptions struct {
302309
ResourceFinder genericclioptions.ResourceFinder
303310
// UIDMap maps a resource location to a UID. It is optional, but ConditionFuncs may choose to use it to make the result
304311
// more reliable. For instance, delete can look for UID consistency during delegated calls.
305-
UIDMap UIDMap
306-
DynamicClient dynamic.Interface
307-
Timeout time.Duration
308-
ForCondition string
312+
UIDMap UIDMap
313+
DynamicClient dynamic.Interface
314+
Timeout time.Duration
315+
ForCondition string
316+
WaitForCreation bool
309317

310318
Printer printers.ResourcePrinter
311319
ConditionFn ConditionFunc
@@ -320,6 +328,40 @@ func (o *WaitOptions) RunWait() error {
320328
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
321329
defer cancel()
322330

331+
isForDelete := strings.ToLower(o.ForCondition) == "delete"
332+
if o.WaitForCreation && o.Timeout == 0 {
333+
return fmt.Errorf("--wait-for-creation requires a timeout value greater than 0")
334+
}
335+
336+
if o.WaitForCreation && !isForDelete {
337+
err := func() error {
338+
for {
339+
select {
340+
case <-ctx.Done():
341+
return fmt.Errorf("context deadline is exceeded while waiting for the creation of the resources")
342+
default:
343+
err := o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error {
344+
// We don't need to do anything after we assure that the resources exist. Because
345+
// actual logic will be incorporated after we wait all the resources' existence.
346+
return nil
347+
})
348+
// It is verified that all the resources exist.
349+
if err == nil {
350+
return nil
351+
}
352+
// We specifically wait for the creation of resources and all the errors
353+
// other than not found means that this is something we cannot handle.
354+
if !apierrors.IsNotFound(err) {
355+
return err
356+
}
357+
}
358+
}
359+
}()
360+
if err != nil {
361+
return err
362+
}
363+
}
364+
323365
visitCount := 0
324366
visitFunc := func(info *resource.Info, err error) error {
325367
if err != nil {
@@ -338,7 +380,6 @@ func (o *WaitOptions) RunWait() error {
338380
return err
339381
}
340382
visitor := o.ResourceFinder.Do()
341-
isForDelete := strings.ToLower(o.ForCondition) == "delete"
342383
if visitor, ok := visitor.(*resource.Result); ok && isForDelete {
343384
visitor.IgnoreErrors(apierrors.IsNotFound)
344385
}

test/cmd/legacy-script.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@ runTests() {
10361036
####################
10371037

10381038
record_command run_wait_tests
1039+
record_command run_wait_with_non_existence_check_tests
10391040

10401041
####################
10411042
# kubectl debug #

test/cmd/wait.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,106 @@ EOF
117117
set +o nounset
118118
set +o errexit
119119
}
120+
121+
run_wait_with_non_existence_check_tests() {
122+
set -o nounset
123+
set -o errexit
124+
125+
kube::log::status "Testing kubectl wait"
126+
127+
create_and_use_new_namespace
128+
129+
### Wait for deletion using --all flag
130+
131+
# create test data
132+
kubectl create deployment test-1 --image=busybox
133+
kubectl create deployment test-2 --image=busybox
134+
135+
# Post-Condition: deployments exists
136+
kube::test::get_object_assert "deployments" "{{range .items}}{{.metadata.name}},{{end}}" 'test-1,test-2,'
137+
138+
# wait with jsonpath will timout for busybox deployment
139+
set +o errexit
140+
# Command: Wait with jsonpath support fields not exist in the first place
141+
output_message=$(kubectl wait --wait-for-creation --for=jsonpath=.status.readyReplicas=1 deploy/test-1 2>&1)
142+
set -o errexit
143+
144+
# Post-Condition: Wait failed
145+
kube::test::if_has_string "${output_message}" 'timed out'
146+
147+
# Delete all deployments async to kubectl wait
148+
( sleep 2 && kubectl delete deployment --all ) &
149+
150+
# Command: Wait for all deployments to be deleted
151+
output_message=$(kubectl wait deployment --for=delete --all)
152+
153+
# Post-Condition: Wait was successful
154+
kube::test::if_has_string "${output_message}" 'test-1 condition met'
155+
kube::test::if_has_string "${output_message}" 'test-2 condition met'
156+
157+
# create test data to test timeout error is occurred in correct time
158+
kubectl apply -f - <<EOF
159+
apiVersion: apps/v1
160+
kind: Deployment
161+
metadata:
162+
labels:
163+
app: dtest
164+
name: dtest
165+
spec:
166+
replicas: 3
167+
selector:
168+
matchLabels:
169+
app: dtest
170+
template:
171+
metadata:
172+
labels:
173+
app: dtest
174+
spec:
175+
containers:
176+
- name: bb
177+
image: busybox
178+
command: ["/bin/sh", "-c", "sleep infinity"]
179+
EOF
180+
181+
set +o errexit
182+
# wait timeout error because condition is invalid
183+
start_sec=$(date +"%s")
184+
output_message=$(time kubectl wait pod --wait-for-creation --selector=app=dtest --for=condition=InvalidCondition --timeout=1s 2>&1)
185+
end_sec=$(date +"%s")
186+
len_sec=$((end_sec-start_sec))
187+
set -o errexit
188+
kube::test::if_has_string "${output_message}" 'timed out waiting for the condition '
189+
test $len_sec -ge 1 && test $len_sec -le 2
190+
191+
# Clean deployment
192+
kubectl delete deployment dtest
193+
194+
# create test data
195+
kubectl create deployment test-3 --image=busybox
196+
197+
# wait with jsonpath without value to succeed
198+
set +o errexit
199+
# Command: Wait with jsonpath without value
200+
output_message_0=$(kubectl wait --wait-for-creation --for=jsonpath='{.status.replicas}' deploy/test-3 2>&1)
201+
# Command: Wait with relaxed jsonpath and filter expression
202+
output_message_1=$(kubectl wait \
203+
--for='jsonpath=spec.template.spec.containers[?(@.name=="busybox")].image=busybox' \
204+
deploy/test-3)
205+
set -o errexit
206+
207+
# Post-Condition: Wait succeed
208+
kube::test::if_has_string "${output_message_0}" 'deployment.apps/test-3 condition met'
209+
kube::test::if_has_string "${output_message_1}" 'deployment.apps/test-3 condition met'
210+
211+
# Clean deployment
212+
kubectl delete deployment test-3
213+
214+
( sleep 3 && kubectl create deployment test-4 --image=busybox ) &
215+
output_message=$(kubectl wait --wait-for-creation --for=jsonpath=.status.replicas=1 deploy/test-4 2>&1)
216+
kube::test::if_has_string "${output_message}" 'test-4 condition met'
217+
218+
kubectl delete deployment test-4
219+
220+
set +o nounset
221+
set +o errexit
222+
}

0 commit comments

Comments
 (0)