Merge pull request #118160 from minherz/master

Support JSONPath condition without value

Kubernetes-commit: 00b8a0a95bb942ad5bfc1bc338452cb983746030
This commit is contained in:
Kubernetes Publisher 2023-07-04 00:26:52 -07:00
commit 0a61782351
2 changed files with 188 additions and 72 deletions

View File

@ -74,6 +74,9 @@ var (
# Wait for the pod "busybox1" to contain the status phase to be "Running" # Wait for the pod "busybox1" to contain the status phase to be "Running"
kubectl wait --for=jsonpath='{.status.phase}'=Running pod/busybox1 kubectl wait --for=jsonpath='{.status.phase}'=Running pod/busybox1
# Wait for the service "loadbalancer" to have ingress.
kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer
# 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`))
@ -120,7 +123,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|--for=jsonpath='{}'=value]", 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,
@ -145,7 +148,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[=condition-value]|jsonpath='{JSONPath expression}'=JSONPath Condition]. The default condition-value is true. Condition values are compared after Unicode simple case folding, which is a more general form of case-insensitivity.") cmd.Flags().StringVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [delete|condition=condition-name[=condition-value]|jsonpath='{JSONPath expression}'=[JSONPath value]]. The default condition-value is true. Condition values are compared after Unicode simple case folding, which is a more general form of case-insensitivity.")
} }
// ToOptions converts from CLI inputs to runtime inputs // ToOptions converts from CLI inputs to runtime inputs
@ -207,10 +210,7 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
} }
if strings.HasPrefix(condition, "jsonpath=") { if strings.HasPrefix(condition, "jsonpath=") {
splitStr := strings.Split(condition, "=") splitStr := strings.Split(condition, "=")
if len(splitStr) != 3 { jsonPathExp, jsonPathValue, err := processJSONPathInput(splitStr[1:])
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 { if err != nil {
return nil, err return nil, err
} }
@ -219,9 +219,10 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
return nil, err return nil, err
} }
return JSONPathWait{ return JSONPathWait{
jsonPathCondition: jsonPathCond, matchAnyValue: jsonPathValue == "",
jsonPathParser: j, jsonPathValue: jsonPathValue,
errOut: errOut, jsonPathParser: j,
errOut: errOut,
}.IsJSONPathConditionMet, nil }.IsJSONPathConditionMet, nil
} }
@ -240,18 +241,23 @@ func newJSONPathParser(jsonPathExpression string) (*jsonpath.JSONPath, error) {
return j, nil return j, nil
} }
// processJSONPathInput will parses the user's JSONPath input and process the string // processJSONPathInput will parses the user's JSONPath input containing JSON expression and, optionally, JSON value for matching condition and process it
func processJSONPathInput(jsonPathExpression, jsonPathCond string) (string, string, error) { func processJSONPathInput(jsonPathInput []string) (string, string, error) {
relaxedJSONPathExp, err := cmdget.RelaxedJSONPathExpression(jsonPathExpression) if numOfArgs := len(jsonPathInput); numOfArgs < 1 || numOfArgs > 2 {
return "", "", fmt.Errorf("jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'")
}
relaxedJSONPathExp, err := cmdget.RelaxedJSONPathExpression(jsonPathInput[0])
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
if jsonPathCond == "" { if len(jsonPathInput) == 1 {
return "", "", errors.New("jsonpath wait condition cannot be empty") return relaxedJSONPathExp, "", nil
} }
jsonPathCond = strings.Trim(jsonPathCond, `'"`) jsonPathValue := strings.Trim(jsonPathInput[1], `'"`)
if jsonPathValue == "" {
return relaxedJSONPathExp, jsonPathCond, nil return "", "", errors.New("jsonpath wait has to have a value after equal sign, like --for=jsonpath='{.status.readyReplicas}'=3")
}
return relaxedJSONPathExp, jsonPathValue, nil
} }
// ResourceLocation holds the location of a resource // ResourceLocation holds the location of a resource
@ -590,8 +596,9 @@ func getObservedGeneration(obj *unstructured.Unstructured, condition map[string]
// JSONPathWait holds a JSONPath Parser which has the ability // JSONPathWait holds a JSONPath Parser which has the ability
// to check for the JSONPath condition and compare with the API server provided JSON output. // to check for the JSONPath condition and compare with the API server provided JSON output.
type JSONPathWait struct { type JSONPathWait struct {
jsonPathCondition string matchAnyValue bool
jsonPathParser *jsonpath.JSONPath jsonPathValue string
jsonPathParser *jsonpath.JSONPath
// errOut is written to if an error occurs // errOut is written to if an error occurs
errOut io.Writer errOut io.Writer
} }
@ -635,7 +642,10 @@ func (j JSONPathWait) checkCondition(obj *unstructured.Unstructured) (bool, erro
if err := verifyParsedJSONPath(parseResults); err != nil { if err := verifyParsedJSONPath(parseResults); err != nil {
return false, err return false, err
} }
isConditionMet, err := compareResults(parseResults[0][0], j.jsonPathCondition) if j.matchAnyValue {
return true, nil
}
isConditionMet, err := compareResults(parseResults[0][0], j.jsonPathValue)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -1027,10 +1027,11 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
fakeClient func() *dynamicfakeclient.FakeDynamicClient fakeClient func() *dynamicfakeclient.FakeDynamicClient
jsonPathExp string jsonPathExp string
jsonPathCond string jsonPathValue string
matchAnyValue bool
expectedErr string expectedErr string
}{ }{
@ -1041,8 +1042,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.foo.bar}", jsonPathExp: "{.foo.bar}",
jsonPathCond: "baz", jsonPathValue: "baz",
matchAnyValue: false,
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
}, },
@ -1053,8 +1055,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.status.containerStatuses[0].ready}", jsonPathExp: "{.status.containerStatuses[0].ready}",
jsonPathCond: "true", jsonPathValue: "true",
matchAnyValue: false,
expectedErr: None, expectedErr: None,
}, },
@ -1065,8 +1068,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.status.containerStatuses[0].ready}", jsonPathExp: "{.status.containerStatuses[0].ready}",
jsonPathCond: "false", jsonPathValue: "false",
matchAnyValue: false,
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
}, },
@ -1077,8 +1081,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.spec.containers[0].ports[0].containerPort}", jsonPathExp: "{.spec.containers[0].ports[0].containerPort}",
jsonPathCond: "80", jsonPathValue: "80",
matchAnyValue: false,
expectedErr: None, expectedErr: None,
}, },
@ -1089,8 +1094,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.spec.containers[0].ports[0].containerPort}", jsonPathExp: "{.spec.containers[0].ports[0].containerPort}",
jsonPathCond: "81", jsonPathValue: "81",
matchAnyValue: false,
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
}, },
@ -1101,11 +1107,41 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.spec.nodeName}", jsonPathExp: "{.spec.nodeName}",
jsonPathCond: "knode0", jsonPathValue: "knode0",
matchAnyValue: false,
expectedErr: None, expectedErr: None,
}, },
{
name: "matches literal value of JSONPath entry without value condition",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.spec.nodeName}",
jsonPathValue: "",
matchAnyValue: true,
expectedErr: None,
},
{
name: "matches complex types map[string]interface{} without value condition",
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}",
jsonPathValue: "",
matchAnyValue: true,
expectedErr: None,
},
{ {
name: "compare string JSONPath entry wrong value", name: "compare string JSONPath entry wrong value",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
@ -1113,8 +1149,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.spec.nodeName}", jsonPathExp: "{.spec.nodeName}",
jsonPathCond: "kmaster", jsonPathValue: "kmaster",
matchAnyValue: false,
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
}, },
@ -1125,8 +1162,22 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.status.conditions[*]}", jsonPathExp: "{.status.conditions[*]}",
jsonPathCond: "foo", jsonPathValue: "foo",
matchAnyValue: false,
expectedErr: "given jsonpath expression matches more than one value",
},
{
name: "matches more than one value without value condition",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{.status.conditions[*]}",
jsonPathValue: "",
matchAnyValue: true,
expectedErr: "given jsonpath expression matches more than one value", expectedErr: "given jsonpath expression matches more than one value",
}, },
@ -1137,8 +1188,22 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}", jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}",
jsonPathCond: "foo", jsonPathValue: "foo",
matchAnyValue: false,
expectedErr: "given jsonpath expression matches more than one list",
},
{
name: "matches more than one list without value condition",
fakeClient: func() *dynamicfakeclient.FakeDynamicClient {
fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping)
fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient
},
jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}",
jsonPathValue: "",
matchAnyValue: true,
expectedErr: "given jsonpath expression matches more than one list", expectedErr: "given jsonpath expression matches more than one list",
}, },
@ -1149,8 +1214,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
fakeClient.PrependReactor("list", "theresource", listReactionfunc) fakeClient.PrependReactor("list", "theresource", listReactionfunc)
return fakeClient return fakeClient
}, },
jsonPathExp: "{.status.conditions}", jsonPathExp: "{.status.conditions}",
jsonPathCond: "True", jsonPathValue: "True",
matchAnyValue: false,
expectedErr: "jsonpath leads to a nested object or list which is not supported", expectedErr: "jsonpath leads to a nested object or list which is not supported",
}, },
@ -1163,8 +1229,9 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
}) })
return fakeClient return fakeClient
}, },
jsonPathExp: "{.spec}", jsonPathExp: "{.spec}",
jsonPathCond: "foo", jsonPathValue: "foo",
matchAnyValue: false,
expectedErr: "jsonpath leads to a nested object or list which is not supported", expectedErr: "jsonpath leads to a nested object or list which is not supported",
}, },
@ -1180,9 +1247,10 @@ func TestWaitForDifferentJSONPathExpression(t *testing.T) {
Printer: printers.NewDiscardingPrinter(), Printer: printers.NewDiscardingPrinter(),
ConditionFn: JSONPathWait{ ConditionFn: JSONPathWait{
jsonPathCondition: test.jsonPathCond, matchAnyValue: test.matchAnyValue,
jsonPathParser: j, jsonPathValue: test.jsonPathValue,
errOut: io.Discard}.IsJSONPathConditionMet, jsonPathParser: j,
errOut: io.Discard}.IsJSONPathConditionMet,
IOStreams: genericiooptions.NewTestIOStreamsDiscard(), IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
} }
@ -1212,12 +1280,12 @@ func TestWaitForJSONPathCondition(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
infos []*resource.Info infos []*resource.Info
fakeClient func() *dynamicfakeclient.FakeDynamicClient fakeClient func() *dynamicfakeclient.FakeDynamicClient
timeout time.Duration timeout time.Duration
jsonPathExp string jsonPathExp string
jsonPathCond string jsonPathValue string
expectedErr string expectedErr string
}{ }{
@ -1240,9 +1308,9 @@ func TestWaitForJSONPathCondition(t *testing.T) {
}) })
return fakeClient return fakeClient
}, },
timeout: 3 * time.Second, timeout: 3 * time.Second,
jsonPathExp: "{.metadata.name}", jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo-b6699dcfb-rnv7t", jsonPathValue: "foo-b6699dcfb-rnv7t",
expectedErr: None, expectedErr: None,
}, },
@ -1330,9 +1398,9 @@ func TestWaitForJSONPathCondition(t *testing.T) {
}) })
return fakeClient return fakeClient
}, },
timeout: 3 * time.Second, timeout: 3 * time.Second,
jsonPathExp: "{.metadata.name}", jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo", // use incorrect name so it'll keep waiting jsonPathValue: "foo", // use incorrect name so it'll keep waiting
expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t",
}, },
@ -1360,9 +1428,9 @@ func TestWaitForJSONPathCondition(t *testing.T) {
}) })
return fakeClient return fakeClient
}, },
timeout: 10 * time.Second, timeout: 10 * time.Second,
jsonPathExp: "{.metadata.name}", jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo-b6699dcfb-rnv7t", jsonPathValue: "foo-b6699dcfb-rnv7t",
expectedErr: None, expectedErr: None,
}, },
@ -1385,9 +1453,9 @@ func TestWaitForJSONPathCondition(t *testing.T) {
}) })
return fakeClient return fakeClient
}, },
timeout: 1 * time.Second, timeout: 1 * time.Second,
jsonPathExp: "{.spec.containers[0].image}", jsonPathExp: "{.spec.containers[0].image}",
jsonPathCond: "nginx", jsonPathValue: "nginx",
expectedErr: None, expectedErr: None,
}, },
@ -1426,9 +1494,9 @@ func TestWaitForJSONPathCondition(t *testing.T) {
}) })
return fakeClient return fakeClient
}, },
timeout: 10 * time.Second, timeout: 10 * time.Second,
jsonPathExp: "{.metadata.name}", jsonPathExp: "{.metadata.name}",
jsonPathCond: "foo-b6699dcfb-rnv7t", jsonPathValue: "foo-b6699dcfb-rnv7t",
expectedErr: None, expectedErr: None,
}, },
@ -1444,8 +1512,8 @@ func TestWaitForJSONPathCondition(t *testing.T) {
Printer: printers.NewDiscardingPrinter(), Printer: printers.NewDiscardingPrinter(),
ConditionFn: JSONPathWait{ ConditionFn: JSONPathWait{
jsonPathCondition: test.jsonPathCond, jsonPathValue: test.jsonPathValue,
jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet, jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet,
IOStreams: genericiooptions.NewTestIOStreamsDiscard(), IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
} }
@ -1465,3 +1533,41 @@ func TestWaitForJSONPathCondition(t *testing.T) {
}) })
} }
} }
// TestWaitForJSONPathBadConditionParsing will test errors in parsing JSONPath bad condition expressions
// except for parsing JSONPath expression itself (i.e. call to cmdget.RelaxedJSONPathExpression())
func TestWaitForJSONPathBadConditionParsing(t *testing.T) {
tests := []struct {
name string
condition string
expectedResult JSONPathWait
expectedErr string
}{
{
name: "missing JSONPath expression",
condition: "jsonpath=",
expectedErr: "jsonpath expression cannot be empty",
},
{
name: "value in JSONPath expression has equal sign",
condition: "jsonpath={.metadata.name}='test=wrong'",
expectedErr: "jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'",
},
{
name: "undefined value",
condition: "jsonpath={.metadata.name}=",
expectedErr: "jsonpath wait has to have a value after equal sign, like --for=jsonpath='{.status.readyReplicas}'=3",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := conditionFuncFor(test.condition, io.Discard)
if err == nil && test.expectedErr != "" {
t.Fatalf("expected %q, got empty", test.expectedErr)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
}
})
}
}