Merge pull request #105776 from lauchokyip/addWaitJson

Add wait json
This commit is contained in:
Kubernetes Prow Robot 2021-11-15 23:35:26 -08:00 committed by GitHub
commit 6be67e860c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 840 additions and 13 deletions

View File

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"io"
"reflect"
"strings"
"time"
@ -40,6 +41,8 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
watchtools "k8s.io/client-go/tools/watch"
"k8s.io/client-go/util/jsonpath"
cmdget "k8s.io/kubectl/pkg/cmd/get"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
@ -65,6 +68,9 @@ var (
# The default value of status condition is true; you can set it to false
kubectl wait --for=condition=Ready=false pod/busybox1
# Wait for the pod "busybox1" to contain the status phase to be "Running".
kubectl wait --for=jsonpath='{.status.phase}'=Running pod/busybox1
# 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`))
@ -111,7 +117,7 @@ func NewCmdWait(restClientGetter genericclioptions.RESTClientGetter, streams gen
flags := NewWaitFlags(restClientGetter, streams)
cmd := &cobra.Command{
Use: "wait ([-f FILENAME] | resource.group/resource.name | resource.group [(-l label | --all)]) [--for=delete|--for condition=available]",
Use: "wait ([-f FILENAME] | resource.group/resource.name | resource.group [(-l label | --all)]) [--for=delete|--for condition=available|--for=jsonpath='{}'=value]",
Short: i18n.T("Experimental: Wait for a specific condition on one or many resources"),
Long: waitLong,
Example: waitExample,
@ -136,7 +142,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: [delete|condition=condition-name]. The default status value of condition-name is true, you can set false with condition=condition-name=false")
cmd.Flags().StringVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [delete|condition=condition-name|jsonpath='{JSONPath expression}'=JSONPath Condition]. The default status value of condition-name is true, you can set false with condition=condition-name=false.")
}
// ToOptions converts from CLI inputs to runtime inputs
@ -196,10 +202,55 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
errOut: errOut,
}.IsConditionMet, nil
}
if strings.HasPrefix(condition, "jsonpath=") {
splitStr := strings.Split(condition, "=")
if len(splitStr) != 3 {
return nil, fmt.Errorf("jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3")
}
jsonPathExp, jsonPathCond, err := processJSONPathInput(splitStr[1], splitStr[2])
if err != nil {
return nil, err
}
j, err := newJSONPathParser(jsonPathExp)
if err != nil {
return nil, err
}
return JSONPathWait{
jsonPathCondition: jsonPathCond,
jsonPathParser: j,
errOut: errOut,
}.IsJSONPathConditionMet, nil
}
return nil, fmt.Errorf("unrecognized condition: %q", condition)
}
// newJSONPathParser will create a new JSONPath parser based on the jsonPathExpression
func newJSONPathParser(jsonPathExpression string) (*jsonpath.JSONPath, error) {
j := jsonpath.New("wait")
if jsonPathExpression == "" {
return nil, errors.New("jsonpath expression cannot be empty")
}
if err := j.Parse(jsonPathExpression); err != nil {
return nil, err
}
return j, nil
}
// processJSONPathInput will parses the user's JSONPath input and process the string
func processJSONPathInput(jsonPathExpression, jsonPathCond string) (string, string, error) {
relaxedJSONPathExp, err := cmdget.RelaxedJSONPathExpression(jsonPathExpression)
if err != nil {
return "", "", err
}
if jsonPathCond == "" {
return "", "", errors.New("jsonpath wait condition cannot be empty")
}
jsonPathCond = strings.Trim(jsonPathCond, `'"`)
return relaxedJSONPathExp, jsonPathCond, nil
}
// ResourceLocation holds the location of a resource
type ResourceLocation struct {
GroupResource schema.GroupResource
@ -353,16 +404,12 @@ func (w Wait) IsDeleted(event watch.Event) (bool, error) {
}
}
// ConditionalWait hold information to check an API status condition
type ConditionalWait struct {
conditionName string
conditionStatus string
// errOut is written to if an error occurs
errOut io.Writer
}
type isCondMetFunc func(event watch.Event) (bool, error)
type checkCondFunc func(obj *unstructured.Unstructured) (bool, error)
// IsConditionMet is a conditionfunc for waiting on an API condition to be met
func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
// getObjAndCheckCondition will make a List query to the API server to get the object and check if the condition is met using check function.
// If the condition is not met, it will make a Watch query to the server and pass in the condMet function
func getObjAndCheckCondition(info *resource.Info, o *WaitOptions, condMet isCondMetFunc, check checkCondFunc) (runtime.Object, bool, error) {
endTime := time.Now().Add(o.Timeout)
for {
if len(info.Name) == 0 {
@ -383,7 +430,7 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
resourceVersion = gottenObjList.GetResourceVersion()
default:
gottenObj = &gottenObjList.Items[0]
conditionMet, err := w.checkCondition(gottenObj)
conditionMet, err := check(gottenObj)
if conditionMet {
return gottenObj, true, nil
}
@ -409,7 +456,7 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
}
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
watchEvent, err := watchtools.UntilWithoutRetry(ctx, objWatch, w.isConditionMet)
watchEvent, err := watchtools.UntilWithoutRetry(ctx, objWatch, watchtools.ConditionFunc(condMet))
cancel()
switch {
case err == nil:
@ -427,6 +474,19 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
}
}
// ConditionalWait hold information to check an API status condition
type ConditionalWait struct {
conditionName string
conditionStatus string
// errOut is written to if an error occurs
errOut io.Writer
}
// IsConditionMet is a conditionfunc for waiting on an API condition to be met
func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
return getObjAndCheckCondition(info, o, w.isConditionMet, w.checkCondition)
}
func (w ConditionalWait) checkCondition(obj *unstructured.Unstructured) (bool, error) {
conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
if err != nil {
@ -486,3 +546,86 @@ func getObservedGeneration(obj *unstructured.Unstructured, condition map[string]
statusObservedGeneration, found, _ := unstructured.NestedInt64(obj.Object, "status", "observedGeneration")
return statusObservedGeneration, found
}
// JSONPathWait holds a JSONPath Parser which has the ability
// to check for the JSONPath condition and compare with the API server provided JSON output.
type JSONPathWait struct {
jsonPathCondition string
jsonPathParser *jsonpath.JSONPath
// errOut is written to if an error occurs
errOut io.Writer
}
// IsJSONPathConditionMet fulfills the requirements of the interface ConditionFunc which provides condition check
func (j JSONPathWait) IsJSONPathConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
return getObjAndCheckCondition(info, o, j.isJSONPathConditionMet, j.checkCondition)
}
// isJSONPathConditionMet is a helper function of IsJSONPathConditionMet
// which check the watch event and check if a JSONPathWait condition is met
func (j JSONPathWait) isJSONPathConditionMet(event watch.Event) (bool, error) {
if event.Type == watch.Error {
// keep waiting in the event we see an error - we expect the watch to be closed by
// the server
err := apierrors.FromObject(event.Object)
fmt.Fprintf(j.errOut, "error: An error occurred while waiting for the condition to be satisfied: %v", err)
return false, nil
}
if event.Type == watch.Deleted {
// this will chain back out, result in another get and an return false back up the chain
return false, nil
}
// event runtime Object can be safely asserted to Unstructed
// because we are working with dynamic client
obj := event.Object.(*unstructured.Unstructured)
return j.checkCondition(obj)
}
// checkCondition uses JSONPath parser to parse the JSON received from the API server
// and check if it matches the desired condition
func (j JSONPathWait) checkCondition(obj *unstructured.Unstructured) (bool, error) {
queryObj := obj.UnstructuredContent()
parseResults, err := j.jsonPathParser.FindResults(queryObj)
if err != nil {
return false, err
}
if err := verifyParsedJSONPath(parseResults); err != nil {
return false, err
}
isConditionMet, err := compareResults(parseResults[0][0], j.jsonPathCondition)
if err != nil {
return false, err
}
return isConditionMet, nil
}
// verifyParsedJSONPath verifies the JSON received from the API server is valid.
// It will only accept a single JSON
func verifyParsedJSONPath(results [][]reflect.Value) error {
if len(results) == 0 {
return errors.New("given jsonpath expression does not match any value")
}
if len(results) > 1 {
return errors.New("given jsonpath expression matches more than one list")
}
if len(results[0]) > 1 {
return errors.New("given jsonpath expression matches more than one value")
}
return nil
}
// compareResults will compare the reflect.Value from the result parsed by the
// JSONPath parser with the expected value given by the value
//
// Since this is coming from an unstructured this can only ever be a primitive,
// map[string]interface{}, or []interface{}.
// We do not support the last two and rely on fmt to handle conversion to string
// and compare the result with user input
func compareResults(r reflect.Value, expectedVal string) (bool, error) {
switch r.Interface().(type) {
case map[string]interface{}, []interface{}:
return false, errors.New("jsonpath leads to a nested object or list which is not supported")
}
s := fmt.Sprintf("%v", r.Interface())
return strings.TrimSpace(s) == strings.TrimSpace(expectedVal), nil
}

View File

@ -23,6 +23,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -30,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
@ -38,6 +40,122 @@ import (
clienttesting "k8s.io/client-go/testing"
)
const (
None string = ""
podYAML string = `
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "1998-10-21T18:39:43Z"
generateName: foo-b6699dcfb-
labels:
app: nginx
pod-template-hash: b6699dcfb
name: foo-b6699dcfb-rnv7t
namespace: default
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: foo-b6699dcfb
uid: 8fc1088c-15d5-4a8c-8502-4dfcedef97b8
resourceVersion: "14203463"
uid: e2cc99fa-5a28-44da-b880-4dded28882ef
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
protocol: TCP
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 250m
memory: 64Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-s64k4
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: knode0
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: kube-api-access-s64k4
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "1998-10-21T18:39:37Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "1998-10-21T18:39:42Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "1998-10-21T18:39:42Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "1998-10-21T18:39:37Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://e35792ba1d6e9a56629659b35dbdb93dacaa0a413962ee04775319f5438e493c
image: docker.io/library/nginx:latest
imageID: docker.io/library/nginx@sha256:644a70516a26004c97d0d85c7fe1d0c3a67ea8ab7ddf4aff193d9f301670cf36
lastState: {}
name: nginx
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "1998-10-21T18:39:41Z"
hostIP: 192.168.0.22
phase: Running
podIP: 10.42.1.203
podIPs:
- ip: 10.42.1.203
qosClass: Burstable
startTime: "1998-10-21T18:39:37Z"
`
)
func newUnstructuredList(items ...*unstructured.Unstructured) *unstructured.UnstructuredList {
list := &unstructured.UnstructuredList{}
for i := range items {
@ -106,6 +224,20 @@ func addConditionWithObservedGeneration(in *unstructured.Unstructured, name, sta
return in
}
// createUnstructured parses the yaml string into a map[string]interface{}. Verifies that the string does not have
// any tab characters.
func createUnstructured(t *testing.T, config string) *unstructured.Unstructured {
t.Helper()
result := map[string]interface{}{}
require.False(t, strings.Contains(config, "\t"), "Yaml %s cannot contain tabs", config)
require.NoError(t, yaml.Unmarshal([]byte(config), &result), "Could not parse config:\n\n%s\n", config)
return &unstructured.Unstructured{
Object: result,
}
}
func TestWaitForDeletion(t *testing.T) {
scheme := runtime.NewScheme()
listMapping := map[schema.GroupVersionResource]string{
@ -1001,3 +1133,555 @@ func TestWaitForDeletionIgnoreNotFound(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
}
// TestWaitForDifferentJSONPathCondition will run tests on different types of
// JSONPath expression to check the JSONPath can be parsed correctly from a Pod Yaml
// and check if the comparison returns as expected.
func TestWaitForDifferentJSONPathExpression(t *testing.T) {
scheme := runtime.NewScheme()
listMapping := map[schema.GroupVersionResource]string{
{Group: "group", Version: "version", Resource: "theresource"}: "TheKindList",
}
listReactionfunc := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructuredList(createUnstructured(t, podYAML)), nil
}
infos := []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
}
tests := []struct {
name string
fakeClient func() *dynamicfakeclient.FakeDynamicClient
jsonPathExp string
jsonPathCond string
expectedErr string
}{
{
name: "JSONPath entry not exist",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.foo.bar}",
jsonPathCond: "baz",
expectedErr: "foo is not found",
},
{
name: "compare boolean JSONPath entry",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.status.containerStatuses[0].ready}",
jsonPathCond: "true",
expectedErr: None,
},
{
name: "compare boolean JSONPath entry wrong value",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.status.containerStatuses[0].ready}",
jsonPathCond: "false",
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
},
{
name: "compare integer JSONPath entry",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.spec.containers[0].ports[0].containerPort}",
jsonPathCond: "80",
expectedErr: None,
},
{
name: "compare integer JSONPath entry wrong value",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.spec.containers[0].ports[0].containerPort}",
jsonPathCond: "81",
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
},
{
name: "compare string JSONPath entry",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.spec.nodeName}",
jsonPathCond: "knode0",
expectedErr: None,
},
{
name: "compare string JSONPath entry wrong value",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.spec.nodeName}",
jsonPathCond: "kmaster",
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
},
{
name: "matches more than one value",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.status.conditions[*]}",
jsonPathCond: "foo",
expectedErr: "given jsonpath expression matches more than one value",
},
{
name: "matches more than one list",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}",
jsonPathCond: "foo",
expectedErr: "given jsonpath expression matches more than one list",
},
{
name: "unsupported type []interface{}",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.status.conditions}",
jsonPathCond: "True",
expectedErr: "jsonpath leads to a nested object or list which is not supported",
},
{
name: "unsupported type map[string]interface{}",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructuredList(createUnstructured(t, podYAML)), nil
})
return fakeClient
},
jsonPathExp: "{.spec}",
jsonPathCond: "foo",
expectedErr: "jsonpath leads to a nested object or list which is not supported",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient()
j, _ := newJSONPathParser(test.jsonPathExp)
o := &WaitOptions{
ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(infos...),
DynamicClient: fakeClient,
Timeout: 1 * time.Millisecond,
Printer: printers.NewDiscardingPrinter(),
ConditionFn: JSONPathWait{
jsonPathCondition: test.jsonPathCond,
jsonPathParser: j,
errOut: ioutil.Discard}.IsJSONPathConditionMet,
IOStreams: genericclioptions.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())
}
}
})
}
}
// TestWaitForJSONPathCondition will run tests to check whether
// the List actions and Watch actions match what we expected
func TestWaitForJSONPathCondition(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
jsonPathExp string
jsonPathCond string
expectedErr string
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "present on get",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructuredList(
createUnstructured(t, podYAML)), nil
})
return fakeClient
},
timeout: 3 * time.Second,
jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo-b6699dcfb-rnv7t",
expectedErr: None,
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 1 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles no infos",
infos: []*resource.Info{},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClient(scheme)
},
timeout: 10 * time.Second,
expectedErr: errNoMatchingResources.Error(),
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 0 {
t.Fatal(spew.Sdump(actions))
}
},
},
{
name: "handles empty object name",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
},
timeout: 10 * time.Second,
expectedErr: "resource name must be provided",
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 0 {
t.Fatal(spew.Sdump(actions))
}
},
},
{
name: "times out",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, createUnstructured(t, podYAML), nil
})
return fakeClient
},
timeout: 1 * time.Second,
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch close out",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
unstructuredObj := createUnstructured(t, podYAML)
unstructuredObj.SetResourceVersion("123")
unstructuredList := newUnstructuredList(unstructuredObj)
unstructuredList.SetResourceVersion("234")
return true, unstructuredList, nil
})
count := 0
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
if count == 0 {
count++
fakeWatch := watch.NewRaceFreeFake()
go func() {
time.Sleep(100 * time.Millisecond)
fakeWatch.Stop()
}()
return true, fakeWatch, nil
}
fakeWatch := watch.NewRaceFreeFake()
return true, fakeWatch, nil
})
return fakeClient
},
timeout: 3 * time.Second,
jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo", // use incorrect name so it'll keep waiting
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 4 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") || actions[1].(clienttesting.WatchAction).GetWatchRestrictions().ResourceVersion != "234" {
t.Error(spew.Sdump(actions))
}
if !actions[2].Matches("list", "theresource") || actions[2].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[3].Matches("watch", "theresource") || actions[3].(clienttesting.WatchAction).GetWatchRestrictions().ResourceVersion != "234" {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch condition change",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
unstructuredObj := createUnstructured(t, podYAML)
unstructuredObj.SetName("foo")
return true, newUnstructuredList(), nil
})
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
fakeWatch := watch.NewRaceFreeFake()
fakeWatch.Action(watch.Modified, createUnstructured(t, podYAML))
return true, fakeWatch, nil
})
return fakeClient
},
timeout: 10 * time.Second,
jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo-b6699dcfb-rnv7t",
expectedErr: None,
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "handles watch created",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
fakeWatch := watch.NewRaceFreeFake()
fakeWatch.Action(watch.Added, createUnstructured(t, podYAML))
return true, fakeWatch, nil
})
return fakeClient
},
timeout: 10 * time.Second,
jsonPathExp: "{.spec.containers[0].image}",
jsonPathCond: "nginx",
expectedErr: None,
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 2 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
{
name: "ignores watch error",
infos: []*resource.Info{
{
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"},
},
Name: "foo-b6699dcfb-rnv7t",
Namespace: "default",
},
},
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil
})
count := 0
fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
fakeWatch := watch.NewRaceFreeFake()
if count == 0 {
fakeWatch.Error(newUnstructuredStatus(&metav1.Status{
TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"},
Status: "Failure",
Code: 500,
Message: "Bad",
}))
fakeWatch.Stop()
} else {
fakeWatch.Action(watch.Modified, createUnstructured(t, podYAML))
}
count++
return true, fakeWatch, nil
})
return fakeClient
},
timeout: 10 * time.Second,
jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo-b6699dcfb-rnv7t",
expectedErr: None,
validateActions: func(t *testing.T, actions []clienttesting.Action) {
if len(actions) != 4 {
t.Fatal(spew.Sdump(actions))
}
if !actions[0].Matches("list", "theresource") || actions[0].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[1].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
if !actions[2].Matches("list", "theresource") || actions[2].(clienttesting.ListAction).GetListRestrictions().Fields.String() != "metadata.name=foo-b6699dcfb-rnv7t" {
t.Error(spew.Sdump(actions))
}
if !actions[3].Matches("watch", "theresource") {
t.Error(spew.Sdump(actions))
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := test.fakeClient()
j, _ := newJSONPathParser(test.jsonPathExp)
o := &WaitOptions{
ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...),
DynamicClient: fakeClient,
Timeout: test.timeout,
Printer: printers.NewDiscardingPrinter(),
ConditionFn: JSONPathWait{
jsonPathCondition: test.jsonPathCond,
jsonPathParser: j, errOut: ioutil.Discard}.IsJSONPathConditionMet,
IOStreams: genericclioptions.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())
}
}
test.validateActions(t, fakeClient.Actions())
})
}
}