Add --for=create option to kubectl wait
Kubernetes-commit: aaf1fb50f32466aa5e845ff423fc4acc8f04c402
This commit is contained in:
parent
1c0cdd03d9
commit
df17d35554
|
@ -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
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||||
"k8s.io/cli-runtime/pkg/printers"
|
"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) {
|
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
|
return IsDeleted, nil
|
||||||
}
|
|
||||||
if strings.HasPrefix(condition, "condition=") {
|
case lowercaseCond == "create":
|
||||||
conditionName := condition[len("condition="):]
|
return IsCreated, nil
|
||||||
|
|
||||||
|
case strings.HasPrefix(lowercaseCond, "condition="):
|
||||||
|
conditionName := lowercaseCond[len("condition="):]
|
||||||
conditionValue := "true"
|
conditionValue := "true"
|
||||||
if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 {
|
if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 {
|
||||||
conditionValue = conditionName[equalsIndex+1:]
|
conditionValue = conditionName[equalsIndex+1:]
|
||||||
|
@ -202,9 +208,9 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
|
||||||
conditionStatus: conditionValue,
|
conditionStatus: conditionValue,
|
||||||
errOut: errOut,
|
errOut: errOut,
|
||||||
}.IsConditionMet, nil
|
}.IsConditionMet, nil
|
||||||
}
|
|
||||||
if strings.HasPrefix(condition, "jsonpath=") {
|
case strings.HasPrefix(lowercaseCond, "jsonpath="):
|
||||||
jsonPathInput := strings.TrimPrefix(condition, "jsonpath=")
|
jsonPathInput := strings.TrimPrefix(lowercaseCond, "jsonpath=")
|
||||||
jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput)
|
jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -312,6 +318,31 @@ 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()
|
||||||
|
|
||||||
|
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
|
visitCount := 0
|
||||||
visitFunc := func(info *resource.Info, err error) error {
|
visitFunc := func(info *resource.Info, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -24,6 +24,8 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
@ -76,7 +78,7 @@ spec:
|
||||||
memory: 128Mi
|
memory: 128Mi
|
||||||
requests:
|
requests:
|
||||||
cpu: 250m
|
cpu: 250m
|
||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
terminationMessagePath: /dev/termination-log
|
terminationMessagePath: /dev/termination-log
|
||||||
terminationMessagePolicy: File
|
terminationMessagePolicy: File
|
||||||
volumeMounts:
|
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) {
|
func TestWaitForDeletionIgnoreNotFound(t *testing.T) {
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
listMapping := map[schema.GroupVersionResource]string{
|
listMapping := map[schema.GroupVersionResource]string{
|
||||||
|
|
Loading…
Reference in New Issue