mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 11:21:47 +00:00
commit
6be67e860c
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user