diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go b/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go index 6cdef2c2f72..be6f9398df6 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go @@ -464,7 +464,7 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error { Timeout: effectiveTimeout, Printer: printers.NewDiscardingPrinter(), - ConditionFn: cmdwait.IsDeleted, + ConditionFn: []cmdwait.ConditionFunc{cmdwait.IsDeleted}, IOStreams: o.IOStreams, } err = waitOptions.RunWaitContext(context.Background()) 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 257d5c89f1b..53a4535c2e7 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io" + "slices" "strings" "time" @@ -79,7 +80,15 @@ 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 pod "busybox1" to be created AND reach the "Ready" status condition + kubectl wait --for=condition=Ready --for=create pod/busybox1 + + # Wait for pod "busybox1" to reach the "Ready" status OR for its containers to report a "False" readiness state + until kubectl wait pod/busybox1 --for=condition=Ready --timeout=1s 2>/dev/null || \ + kubectl wait pod/busybox1 --for=condition=ContainersReady=False --timeout=1s 2>/dev/null; \ + do echo "Checking conditions..."; sleep 1; done`)) ) // errNoMatchingResources is returned when there is no resources matching a query. @@ -94,7 +103,7 @@ type WaitFlags struct { ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags Timeout time.Duration - ForCondition string + ForCondition []string genericiooptions.IOStreams } @@ -147,7 +156,7 @@ 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: [create|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().StringArrayVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [create|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. Multiple conditions are supported and AND'ed to each other in a sequential order. If --for=create is passed, it is always waited first.") } // ToOptions converts from CLI inputs to runtime inputs @@ -165,7 +174,7 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) { if err != nil { return nil, err } - conditionFn, err := conditionFuncFor(flags.ForCondition, flags.ErrOut) + conditionFn, err := conditionFuncsFor(flags.ForCondition, flags.ErrOut) if err != nil { return nil, err } @@ -189,48 +198,56 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) { return o, nil } -func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error) { - lowercaseCond := strings.ToLower(condition) - switch { - case lowercaseCond == "delete": - return IsDeleted, nil +func conditionFuncsFor(conditions []string, errOut io.Writer) ([]ConditionFunc, error) { + var condFuncs []ConditionFunc + for _, cond := range conditions { + lowercaseCond := strings.ToLower(cond) + switch { + case lowercaseCond == "delete": + condFuncs = append(condFuncs, IsDeleted) - case lowercaseCond == "create": - return IsCreated, nil + case lowercaseCond == "create": + condFuncs = append(condFuncs, IsCreated) - case strings.HasPrefix(condition, "condition="): - conditionName := strings.TrimPrefix(condition, "condition=") - conditionValue := "true" - if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 { - conditionValue = conditionName[equalsIndex+1:] - conditionName = conditionName[0:equalsIndex] + case strings.HasPrefix(cond, "condition="): + conditionName := strings.TrimPrefix(cond, "condition=") + conditionValue := "true" + if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 { + conditionValue = conditionName[equalsIndex+1:] + conditionName = conditionName[0:equalsIndex] + } + + condFuncs = append(condFuncs, ConditionalWait{ + conditionName: conditionName, + conditionStatus: conditionValue, + errOut: errOut, + }.IsConditionMet) + + case strings.HasPrefix(cond, "jsonpath="): + jsonPathInput := strings.TrimPrefix(cond, "jsonpath=") + jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput) + if err != nil { + return nil, err + } + j, err := newJSONPathParser(jsonPathExp) + if err != nil { + return nil, err + } + condFuncs = append(condFuncs, JSONPathWait{ + matchAnyValue: jsonPathValue == "", + jsonPathValue: jsonPathValue, + jsonPathParser: j, + errOut: errOut, + }.IsJSONPathConditionMet) + default: + return nil, fmt.Errorf("unrecognized condition: %q", cond) } - - return ConditionalWait{ - conditionName: conditionName, - conditionStatus: conditionValue, - errOut: errOut, - }.IsConditionMet, nil - - case strings.HasPrefix(condition, "jsonpath="): - jsonPathInput := strings.TrimPrefix(condition, "jsonpath=") - jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput) - if err != nil { - return nil, err - } - j, err := newJSONPathParser(jsonPathExp) - if err != nil { - return nil, err - } - return JSONPathWait{ - matchAnyValue: jsonPathValue == "", - jsonPathValue: jsonPathValue, - jsonPathParser: j, - errOut: errOut, - }.IsJSONPathConditionMet, nil + } + if condFuncs == nil { + return nil, fmt.Errorf("unrecognized condition: %q", conditions) } - return nil, fmt.Errorf("unrecognized condition: %q", condition) + return condFuncs, nil } // newJSONPathParser will create a new JSONPath parser based on the jsonPathExpression @@ -306,10 +323,10 @@ type WaitOptions struct { UIDMap UIDMap DynamicClient dynamic.Interface Timeout time.Duration - ForCondition string + ForCondition []string Printer printers.ResourcePrinter - ConditionFn ConditionFunc + ConditionFn []ConditionFunc genericiooptions.IOStreams } @@ -325,7 +342,7 @@ func (o *WaitOptions) RunWait() error { func (o *WaitOptions) RunWaitContext(ctx context.Context) error { ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, o.Timeout) defer cancel() - if strings.ToLower(o.ForCondition) == "create" { + if containsCondition(o.ForCondition, "create") { // TODO(soltysh): this is not ideal solution, because we're polling every .5s, // and we have to use ResourceFinder, which contains the resource name. // In the long run, we should expose resource information from ResourceFinder, @@ -359,18 +376,21 @@ func (o *WaitOptions) RunWaitContext(ctx context.Context) error { } visitCount++ - finalObject, success, err := o.ConditionFn(ctx, info, o) - if success { - o.Printer.PrintObj(finalObject, o.Out) - return nil + for _, condFn := range o.ConditionFn { + finalObject, success, err := condFn(ctx, info, o) + if success { + o.Printer.PrintObj(finalObject, o.Out) //nolint:errcheck + continue + } + if err == nil { + return fmt.Errorf("%v unsatisfied for unknown reason", finalObject) + } + return err } - if err == nil { - return fmt.Errorf("%v unsatisfied for unknown reason", finalObject) - } - return err + return nil } visitor := o.ResourceFinder.Do() - isForDelete := strings.ToLower(o.ForCondition) == "delete" + isForDelete := containsCondition(o.ForCondition, "delete") if visitor, ok := visitor.(*resource.Result); ok && isForDelete { visitor.IgnoreErrors(apierrors.IsNotFound) } @@ -384,3 +404,9 @@ func (o *WaitOptions) RunWaitContext(ctx context.Context) error { } return err } + +func containsCondition(conditions []string, condition string) bool { + return slices.ContainsFunc(conditions, func(cond string) bool { + return strings.ToLower(cond) == condition + }) +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go index 4a4ca1ceb73..37d127c1bcc 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go @@ -596,7 +596,7 @@ func TestWaitForDeletion(t *testing.T) { Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), - ConditionFn: IsDeleted, + ConditionFn: []ConditionFunc{IsDeleted}, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWaitContext(t.Context()) @@ -1006,7 +1006,7 @@ func TestWaitForCondition(t *testing.T) { Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), - ConditionFn: ConditionalWait{conditionName: "the-condition", conditionStatus: "status-value", errOut: io.Discard}.IsConditionMet, + ConditionFn: []ConditionFunc{ConditionalWait{conditionName: "the-condition", conditionStatus: "status-value", errOut: io.Discard}.IsConditionMet}, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWaitContext(t.Context()) @@ -1076,8 +1076,8 @@ func TestWaitForCreate(t *testing.T) { Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), - ConditionFn: IsCreated, - ForCondition: "create", + ConditionFn: []ConditionFunc{IsCreated}, + ForCondition: []string{"create"}, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWaitContext(t.Context()) @@ -1108,9 +1108,9 @@ func TestWaitForDeletionIgnoreNotFound(t *testing.T) { ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(infos...), DynamicClient: fakeClient, Printer: printers.NewDiscardingPrinter(), - ConditionFn: IsDeleted, + ConditionFn: []ConditionFunc{IsDeleted}, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), - ForCondition: "delete", + ForCondition: []string{"delete"}, } err := o.RunWaitContext(t.Context()) if err != nil { @@ -1359,11 +1359,11 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) { Timeout: 1 * time.Second, Printer: printers.NewDiscardingPrinter(), - ConditionFn: JSONPathWait{ + ConditionFn: []ConditionFunc{JSONPathWait{ matchAnyValue: test.matchAnyValue, jsonPathValue: test.jsonPathValue, jsonPathParser: j, - errOut: io.Discard}.IsJSONPathConditionMet, + errOut: io.Discard}.IsJSONPathConditionMet}, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } @@ -1624,9 +1624,9 @@ func TestWaitForJSONPathCondition(t *testing.T) { Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), - ConditionFn: JSONPathWait{ + ConditionFn: []ConditionFunc{JSONPathWait{ jsonPathValue: test.jsonPathValue, - jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet, + jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet}, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } @@ -1651,99 +1651,114 @@ func TestWaitForJSONPathCondition(t *testing.T) { func TestConditionFuncFor(t *testing.T) { tests := []struct { name string - condition string + condition []string expectedErr string }{ { name: "jsonpath missing JSONPath expression", - condition: "jsonpath=", + condition: []string{"jsonpath="}, expectedErr: "jsonpath expression cannot be empty", }, { name: "jsonpath check for condition without value", - condition: "jsonpath={.metadata.name}", + condition: []string{"jsonpath={.metadata.name}"}, expectedErr: None, }, { name: "jsonpath check for condition without value relaxed parsing", - condition: "jsonpath=abc", + condition: []string{"jsonpath=abc"}, expectedErr: None, }, { name: "jsonpath check for expression and value", - condition: "jsonpath={.metadata.name}=foo-b6699dcfb-rnv7t", + condition: []string{"jsonpath={.metadata.name}=foo-b6699dcfb-rnv7t"}, expectedErr: None, }, { name: "jsonpath check for expression and value relaxed parsing", - condition: "jsonpath=.metadata.name=foo-b6699dcfb-rnv7t", + condition: []string{"jsonpath=.metadata.name=foo-b6699dcfb-rnv7t"}, expectedErr: None, }, { name: "jsonpath selecting based on condition", - condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}=True`, + condition: []string{`jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}=True`}, expectedErr: None, }, { name: "jsonpath selecting based on condition relaxed parsing", - condition: "jsonpath=status.conditions[?(@.type==\"Available\")].status=True", + condition: []string{"jsonpath=status.conditions[?(@.type==\"Available\")].status=True"}, expectedErr: None, }, { name: "jsonpath selecting based on condition without value", - condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}`, + condition: []string{`jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}`}, expectedErr: None, }, { name: "jsonpath selecting based on condition without value relaxed parsing", - condition: `jsonpath=.status.containerStatuses[?(@.name=="foo")].ready`, + condition: []string{`jsonpath=.status.containerStatuses[?(@.name=="foo")].ready`}, expectedErr: None, }, { name: "jsonpath invalid expression with repeated '='", - condition: "jsonpath={.metadata.name}='test=wrong'", + condition: []string{"jsonpath={.metadata.name}='test=wrong'"}, expectedErr: "jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'", }, { name: "jsonpath undefined value after '='", - condition: "jsonpath={.metadata.name}=", + condition: []string{"jsonpath={.metadata.name}="}, expectedErr: "jsonpath wait has to have a value after equal sign", }, { name: "jsonpath complex expressions not supported", - condition: "jsonpath={.status.conditions[?(@.type==\"Failed\"||@.type==\"Complete\")].status}=True", + condition: []string{"jsonpath={.status.conditions[?(@.type==\"Failed\"||@.type==\"Complete\")].status}=True"}, expectedErr: "unrecognized character in action: U+007C '|'", }, { name: "jsonpath invalid expression", - condition: "jsonpath={=True", + condition: []string{"jsonpath={=True"}, expectedErr: "unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or " + "'{.name1.name2}'", }, { name: "condition delete", - condition: "delete", + condition: []string{"delete"}, expectedErr: None, }, { name: "condition true", - condition: "condition=hello", + condition: []string{"condition=hello"}, expectedErr: None, }, { name: "condition with value", - condition: "condition=hello=world", + condition: []string{"condition=hello=world"}, expectedErr: None, }, { name: "unrecognized condition", - condition: "cond=invalid", + condition: []string{"cond=invalid"}, expectedErr: "unrecognized condition: \"cond=invalid\"", }, + { + name: "multiple conditions - two status conditions", + condition: []string{"condition=Ready", "condition=Available"}, + expectedErr: None, + }, + { + name: "multiple conditions - condition and jsonpath", + condition: []string{"condition=Ready", "jsonpath={.status.phase}=Running"}, + expectedErr: None, + }, + { + name: "multiple conditions - two jsonpaths", + condition: []string{"jsonpath={.status.phase}=Running", "jsonpath={.metadata.name}=foo"}, + expectedErr: None, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := conditionFuncFor(test.condition, io.Discard) + _, err := conditionFuncsFor(test.condition, io.Discard) switch { case err == nil && test.expectedErr != None: t.Fatalf("expected error %q, got nil", test.expectedErr) @@ -1757,3 +1772,155 @@ func TestConditionFuncFor(t *testing.T) { }) } } + +// TestWaitForMultipleConditions tests that multiple conditions are evaluated with AND logic. +func TestWaitForMultipleConditions(t *testing.T) { + scheme := runtime.NewScheme() + listMapping := map[schema.GroupVersionResource]string{ + {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", + } + + tests := []struct { + name string + infos []*resource.Info + fakeClient func() *dynamicfakeclient.FakeDynamicClient + timeout time.Duration + expectedErr string + }{ + { + name: "both conditions met - success", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + obj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") + obj = addCondition(obj, "Ready", "True") + obj = addCondition(obj, "Available", "True") + return true, newUnstructuredList(obj), nil + }) + return fakeClient + }, + timeout: 10 * time.Second, + }, + { + name: "only first condition met - failure", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + obj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") + obj = addCondition(obj, "Ready", "True") + obj = addCondition(obj, "Available", "False") + return true, newUnstructuredList(obj), nil + }) + return fakeClient + }, + timeout: 1 * time.Second, + expectedErr: "timed out waiting for the condition", + }, + { + name: "conditions met via watch - success", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + obj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") + obj = addCondition(obj, "Ready", "False") + return true, newUnstructuredList(obj), nil + }) + fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { + fakeWatch := watch.NewRaceFreeFake() + obj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") + obj = addCondition(obj, "Ready", "True") + obj = addCondition(obj, "Available", "True") + fakeWatch.Action(watch.Modified, obj) + return true, fakeWatch, nil + }) + return fakeClient + }, + timeout: 10 * time.Second, + }, + { + name: "mixed condition types - condition and jsonpath both met", + infos: []*resource.Info{ + { + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, + }, + Name: "name-foo", + Namespace: "ns-foo", + }, + }, + fakeClient: func() *dynamicfakeclient.FakeDynamicClient { + fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) + fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + obj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") + obj = addCondition(obj, "Ready", "True") + unstructured.SetNestedField(obj.Object, "Running", "status", "phase") //nolint:errcheck + return true, newUnstructuredList(obj), nil + }) + return fakeClient + }, + timeout: 10 * time.Second, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeClient := test.fakeClient() + conditionFuncs, err := conditionFuncsFor([]string{"condition=Ready", "condition=Available"}, io.Discard) + require.NoError(t, err) + + // For the mixed condition test, use different condition functions + if strings.Contains(test.name, "mixed condition") { + conditionFuncs, err = conditionFuncsFor([]string{"condition=Ready", "jsonpath={.status.phase}=Running"}, io.Discard) + require.NoError(t, err) + } + + o := &WaitOptions{ + ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...), + DynamicClient: fakeClient, + Timeout: test.timeout, + Printer: printers.NewDiscardingPrinter(), + ConditionFn: conditionFuncs, + IOStreams: genericiooptions.NewTestIOStreamsDiscard(), + } + err = o.RunWaitContext(t.Context()) + switch { + case err == nil && len(test.expectedErr) == 0: + case err != nil && len(test.expectedErr) == 0: + t.Fatal(err) + case err == nil && len(test.expectedErr) != 0: + t.Fatalf("missing: %q", test.expectedErr) + case err != nil && len(test.expectedErr) != 0: + if !strings.Contains(err.Error(), test.expectedErr) { + t.Fatalf("expected %q, got %q", test.expectedErr, err.Error()) + } + } + }) + } +} diff --git a/test/cmd/wait.sh b/test/cmd/wait.sh index 01820b66778..922ce0e2618 100644 --- a/test/cmd/wait.sh +++ b/test/cmd/wait.sh @@ -42,7 +42,7 @@ run_wait_tests() { # 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 + # wait with jsonpath will timeout for busybox deployment set +o errexit # Command: Wait with jsonpath support fields not exist in the first place output_message=$(kubectl wait --for=jsonpath=.status.readyReplicas=1 deploy/test-1 2>&1) @@ -173,6 +173,114 @@ EOF # Clean deployment kubectl delete deployment test-3 + kube::log::status "Testing kubectl wait with multiple conditions" + + # Test 1: Wait for deployment creation AND available condition together + # Create deployment in background +( sleep 2 && cat <&1) + set -o errexit + + # Post-Condition: Should timeout because second condition not met + kube::test::if_has_string "${output_message}" 'timed out waiting for the condition' + + # Clean up + kubectl delete deployment partial-condition-test + set +o nounset set +o errexit }