Add --for=create option to kubectl wait

This commit is contained in:
Maciej Szulik 2024-07-08 13:32:31 +02:00
parent 6eec9d6b21
commit aaf1fb50f3
No known key found for this signature in database
GPG Key ID: F15E55D276FA84C4
4 changed files with 154 additions and 9 deletions

View File

@ -0,0 +1,33 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wait
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
)
// IsCreated is a condition func for waiting for something to be created
func IsCreated(ctx context.Context, info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
if len(info.Name) == 0 || info.Object == nil {
return nil, false, fmt.Errorf("resource name must be provided")
}
return info.Object, true, nil
}

View File

@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers"
@ -186,11 +187,16 @@ func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) {
}
func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error) {
if strings.ToLower(condition) == "delete" {
lowercaseCond := strings.ToLower(condition)
switch {
case lowercaseCond == "delete":
return IsDeleted, nil
}
if strings.HasPrefix(condition, "condition=") {
conditionName := condition[len("condition="):]
case lowercaseCond == "create":
return IsCreated, nil
case strings.HasPrefix(lowercaseCond, "condition="):
conditionName := lowercaseCond[len("condition="):]
conditionValue := "true"
if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 {
conditionValue = conditionName[equalsIndex+1:]
@ -202,9 +208,9 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
conditionStatus: conditionValue,
errOut: errOut,
}.IsConditionMet, nil
}
if strings.HasPrefix(condition, "jsonpath=") {
jsonPathInput := strings.TrimPrefix(condition, "jsonpath=")
case strings.HasPrefix(lowercaseCond, "jsonpath="):
jsonPathInput := strings.TrimPrefix(lowercaseCond, "jsonpath=")
jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput)
if err != nil {
return nil, err
@ -312,6 +318,31 @@ func (o *WaitOptions) RunWait() error {
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
defer cancel()
if strings.ToLower(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,
// or functions from ResourceBuilder for parsing those. Lastly, this poll
// should be replaced with a ListWatch cache.
if err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, o.Timeout, true, func(context.Context) (done bool, err error) {
visitErr := o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error {
return nil
})
if apierrors.IsNotFound(visitErr) {
return false, nil
}
if visitErr != nil {
return false, visitErr
}
return true, nil
}); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("%s", wait.ErrWaitTimeout.Error()) // nolint:staticcheck // SA1019
}
return err
}
}
visitCount := 0
visitFunc := func(info *resource.Info, err error) error {
if err != nil {

View File

@ -24,6 +24,8 @@ import (
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -76,7 +78,7 @@ spec:
memory: 128Mi
requests:
cpu: 250m
memory: 64Mi
memory: 64Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
@ -983,6 +985,77 @@ func TestWaitForCondition(t *testing.T) {
}
}
func TestWaitForCreate(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
infosErr error
fakeClient func() *dynamicfakeclient.FakeDynamicClient
timeout time.Duration
expectedErr string
}{
{
name: "missing resource, should hit timeout",
infosErr: apierrors.NewNotFound(schema.GroupResource{Group: "group", Resource: "theresource"}, "name-foo"),
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
},
timeout: 1 * time.Second,
expectedErr: "timed out waiting for the condition",
},
{
name: "wait should succeed",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Object: &corev1.Pod{}, // the resource type is irrelevant here
Name: "name-foo",
Namespace: "ns-foo",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
},
timeout: 1 * time.Second,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient()
o := &WaitOptions{
ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...).WithError(test.infosErr),
DynamicClient: fakeClient,
Timeout: test.timeout,
Printer: printers.NewDiscardingPrinter(),
ConditionFn: IsCreated,
ForCondition: "create",
IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
}
err := o.RunWait()
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())
}
}
})
}
}
func TestWaitForDeletionIgnoreNotFound(t *testing.T) {
scheme := runtime.NewScheme()
listMapping := map[schema.GroupVersionResource]string{

View File

@ -26,7 +26,14 @@ run_wait_tests() {
create_and_use_new_namespace
### Wait for deletion using --all flag
# wait --for=create should time out
set +o errexit
# Command: Wait with jsonpath support fields not exist in the first place
output_message=$(kubectl wait --for=create deploy/test-1 --timeout=1s 2>&1)
set -o errexit
# Post-Condition: Wait failed
kube::test::if_has_string "${output_message}" 'timed out waiting for the condition'
# create test data
kubectl create deployment test-1 --image=busybox
@ -120,3 +127,4 @@ EOF
set +o nounset
set +o errexit
}