mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
commit
6be67e860c
@ -21,6 +21,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -40,6 +41,8 @@ import (
|
|||||||
"k8s.io/cli-runtime/pkg/resource"
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
watchtools "k8s.io/client-go/tools/watch"
|
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"
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
"k8s.io/kubectl/pkg/util/i18n"
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
"k8s.io/kubectl/pkg/util/templates"
|
"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
|
# The default value of status condition is true; you can set it to false
|
||||||
kubectl wait --for=condition=Ready=false pod/busybox1
|
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
|
# Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command
|
||||||
kubectl delete pod/busybox1
|
kubectl delete pod/busybox1
|
||||||
kubectl wait --for=delete pod/busybox1 --timeout=60s`))
|
kubectl wait --for=delete pod/busybox1 --timeout=60s`))
|
||||||
@ -111,7 +117,7 @@ func NewCmdWait(restClientGetter genericclioptions.RESTClientGetter, streams gen
|
|||||||
flags := NewWaitFlags(restClientGetter, streams)
|
flags := NewWaitFlags(restClientGetter, streams)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
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"),
|
Short: i18n.T("Experimental: Wait for a specific condition on one or many resources"),
|
||||||
Long: waitLong,
|
Long: waitLong,
|
||||||
Example: waitExample,
|
Example: waitExample,
|
||||||
@ -136,7 +142,7 @@ func (flags *WaitFlags) AddFlags(cmd *cobra.Command) {
|
|||||||
flags.ResourceBuilderFlags.AddFlags(cmd.Flags())
|
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().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
|
// ToOptions converts from CLI inputs to runtime inputs
|
||||||
@ -196,10 +202,55 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
|
|||||||
errOut: errOut,
|
errOut: errOut,
|
||||||
}.IsConditionMet, nil
|
}.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)
|
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
|
// ResourceLocation holds the location of a resource
|
||||||
type ResourceLocation struct {
|
type ResourceLocation struct {
|
||||||
GroupResource schema.GroupResource
|
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 isCondMetFunc func(event watch.Event) (bool, error)
|
||||||
type ConditionalWait struct {
|
type checkCondFunc func(obj *unstructured.Unstructured) (bool, error)
|
||||||
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
|
// getObjAndCheckCondition will make a List query to the API server to get the object and check if the condition is met using check function.
|
||||||
func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) {
|
// 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)
|
endTime := time.Now().Add(o.Timeout)
|
||||||
for {
|
for {
|
||||||
if len(info.Name) == 0 {
|
if len(info.Name) == 0 {
|
||||||
@ -383,7 +430,7 @@ func (w ConditionalWait) IsConditionMet(info *resource.Info, o *WaitOptions) (ru
|
|||||||
resourceVersion = gottenObjList.GetResourceVersion()
|
resourceVersion = gottenObjList.GetResourceVersion()
|
||||||
default:
|
default:
|
||||||
gottenObj = &gottenObjList.Items[0]
|
gottenObj = &gottenObjList.Items[0]
|
||||||
conditionMet, err := w.checkCondition(gottenObj)
|
conditionMet, err := check(gottenObj)
|
||||||
if conditionMet {
|
if conditionMet {
|
||||||
return gottenObj, true, nil
|
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)
|
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()
|
cancel()
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
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) {
|
func (w ConditionalWait) checkCondition(obj *unstructured.Unstructured) (bool, error) {
|
||||||
conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
|
conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -486,3 +546,86 @@ func getObservedGeneration(obj *unstructured.Unstructured, condition map[string]
|
|||||||
statusObservedGeneration, found, _ := unstructured.NestedInt64(obj.Object, "status", "observedGeneration")
|
statusObservedGeneration, found, _ := unstructured.NestedInt64(obj.Object, "status", "observedGeneration")
|
||||||
return statusObservedGeneration, found
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"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"
|
||||||
@ -30,6 +31,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/yaml"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
"k8s.io/cli-runtime/pkg/printers"
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
@ -38,6 +40,122 @@ import (
|
|||||||
clienttesting "k8s.io/client-go/testing"
|
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 {
|
func newUnstructuredList(items ...*unstructured.Unstructured) *unstructured.UnstructuredList {
|
||||||
list := &unstructured.UnstructuredList{}
|
list := &unstructured.UnstructuredList{}
|
||||||
for i := range items {
|
for i := range items {
|
||||||
@ -106,6 +224,20 @@ func addConditionWithObservedGeneration(in *unstructured.Unstructured, name, sta
|
|||||||
return in
|
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) {
|
func TestWaitForDeletion(t *testing.T) {
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
listMapping := map[schema.GroupVersionResource]string{
|
listMapping := map[schema.GroupVersionResource]string{
|
||||||
@ -1001,3 +1133,555 @@ func TestWaitForDeletionIgnoreNotFound(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
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