Add --for=create option to kubectl wait

Kubernetes-commit: aaf1fb50f32466aa5e845ff423fc4acc8f04c402
This commit is contained in:
Maciej Szulik 2024-07-08 13:32:31 +02:00 committed by Kubernetes Publisher
parent 1c0cdd03d9
commit df17d35554
3 changed files with 145 additions and 8 deletions

33
pkg/cmd/wait/create.go Normal file
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{