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/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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue