Improve conditionFuncFor expression parsing for wait --for jsonpath
Make it possible to parse jsonpath filter expressions: Split jsonpath expressions on single '=' only and leave '==' as part of the string. Reported-at: https://github.com/kubernetes/kubernetes/issues/119206 Signed-off-by: Andreas Karis <ak.karis@gmail.com>
This commit is contained in:
parent
7581ae8123
commit
4188998430
@ -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 pod "busybox1" to be Ready
|
||||||
|
kubectl wait --for='jsonpath={.status.conditions[?(@.type=="Ready")].status}=True' pod/busybox1
|
||||||
|
|
||||||
# Wait for the service "loadbalancer" to have ingress.
|
# Wait for the service "loadbalancer" to have ingress.
|
||||||
kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer
|
kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer
|
||||||
|
|
||||||
@ -209,8 +212,8 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
|
|||||||
}.IsConditionMet, nil
|
}.IsConditionMet, nil
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(condition, "jsonpath=") {
|
if strings.HasPrefix(condition, "jsonpath=") {
|
||||||
splitStr := strings.Split(condition, "=")
|
jsonPathInput := strings.TrimPrefix(condition, "jsonpath=")
|
||||||
jsonPathExp, jsonPathValue, err := processJSONPathInput(splitStr[1:])
|
jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -241,8 +244,10 @@ func newJSONPathParser(jsonPathExpression string) (*jsonpath.JSONPath, error) {
|
|||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processJSONPathInput will parses the user's JSONPath input containing JSON expression and, optionally, JSON value for matching condition and process it
|
// processJSONPathInput will parse and process the provided JSONPath input containing a JSON expression and optionally
|
||||||
func processJSONPathInput(jsonPathInput []string) (string, string, error) {
|
// a value for the matching condition.
|
||||||
|
func processJSONPathInput(input string) (string, string, error) {
|
||||||
|
jsonPathInput := splitJSONPathInput(input)
|
||||||
if numOfArgs := len(jsonPathInput); numOfArgs < 1 || numOfArgs > 2 {
|
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}'")
|
return "", "", fmt.Errorf("jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'")
|
||||||
}
|
}
|
||||||
@ -260,6 +265,27 @@ func processJSONPathInput(jsonPathInput []string) (string, string, error) {
|
|||||||
return relaxedJSONPathExp, jsonPathValue, nil
|
return relaxedJSONPathExp, jsonPathValue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splitJSONPathInput splits the provided input string on single '='. Double '==' will not cause the string to be
|
||||||
|
// split. E.g., "a.b.c====d.e.f===g.h.i===" will split to ["a.b.c====d.e.f==","g.h.i==",""].
|
||||||
|
func splitJSONPathInput(input string) []string {
|
||||||
|
var output []string
|
||||||
|
var element strings.Builder
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
if input[i] == '=' {
|
||||||
|
if i < len(input)-1 && input[i+1] == '=' {
|
||||||
|
element.WriteString("==")
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
output = append(output, element.String())
|
||||||
|
element.Reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
element.WriteByte(input[i])
|
||||||
|
}
|
||||||
|
return append(output, element.String())
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -1534,39 +1534,112 @@ func TestWaitForJSONPathCondition(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWaitForJSONPathBadConditionParsing will test errors in parsing JSONPath bad condition expressions
|
// TestConditionFuncFor tests that the condition string can be properly parsed into a ConditionFunc.
|
||||||
// except for parsing JSONPath expression itself (i.e. call to cmdget.RelaxedJSONPathExpression())
|
func TestConditionFuncFor(t *testing.T) {
|
||||||
func TestWaitForJSONPathBadConditionParsing(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
condition string
|
condition string
|
||||||
expectedResult JSONPathWait
|
expectedErr string
|
||||||
expectedErr string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing JSONPath expression",
|
name: "jsonpath missing JSONPath expression",
|
||||||
condition: "jsonpath=",
|
condition: "jsonpath=",
|
||||||
expectedErr: "jsonpath expression cannot be empty",
|
expectedErr: "jsonpath expression cannot be empty",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "value in JSONPath expression has equal sign",
|
name: "jsonpath check for condition without value",
|
||||||
|
condition: "jsonpath={.metadata.name}",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath check for condition without value relaxed parsing",
|
||||||
|
condition: "jsonpath=abc",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath check for expression and value",
|
||||||
|
condition: "jsonpath={.metadata.name}=foo-b6699dcfb-rnv7t",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath check for expression and value relaxed parsing",
|
||||||
|
condition: "jsonpath=.metadata.name=foo-b6699dcfb-rnv7t",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath selecting based on condition",
|
||||||
|
condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}=True`,
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath selecting based on condition relaxed parsing",
|
||||||
|
condition: "jsonpath=status.conditions[?(@.type==\"Available\")].status=True",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath selecting based on condition without value",
|
||||||
|
condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}`,
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath selecting based on condition without value relaxed parsing",
|
||||||
|
condition: `jsonpath=.status.containerStatuses[?(@.name=="foo")].ready`,
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath invalid expression with repeated '='",
|
||||||
condition: "jsonpath={.metadata.name}='test=wrong'",
|
condition: "jsonpath={.metadata.name}='test=wrong'",
|
||||||
expectedErr: "jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'",
|
expectedErr: "jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "undefined value",
|
name: "jsonpath undefined value after '='",
|
||||||
condition: "jsonpath={.metadata.name}=",
|
condition: "jsonpath={.metadata.name}=",
|
||||||
expectedErr: "jsonpath wait has to have a value after equal sign, like --for=jsonpath='{.status.readyReplicas}'=3",
|
expectedErr: "jsonpath wait has to have a value after equal sign",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath complex expressions not supported",
|
||||||
|
condition: "jsonpath={.status.conditions[?(@.type==\"Failed\"||@.type==\"Complete\")].status}=True",
|
||||||
|
expectedErr: "unrecognized character in action: U+007C '|'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonpath invalid expression",
|
||||||
|
condition: "jsonpath={=True",
|
||||||
|
expectedErr: "unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or " +
|
||||||
|
"'{.name1.name2}'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "condition delete",
|
||||||
|
condition: "delete",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "condition true",
|
||||||
|
condition: "condition=hello",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "condition with value",
|
||||||
|
condition: "condition=hello=world",
|
||||||
|
expectedErr: None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unrecognized condition",
|
||||||
|
condition: "cond=invalid",
|
||||||
|
expectedErr: "unrecognized condition: \"cond=invalid\"",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
_, err := conditionFuncFor(test.condition, io.Discard)
|
_, err := conditionFuncFor(test.condition, io.Discard)
|
||||||
if err == nil && test.expectedErr != "" {
|
switch {
|
||||||
t.Fatalf("expected %q, got empty", test.expectedErr)
|
case err == nil && test.expectedErr != None:
|
||||||
}
|
t.Fatalf("expected error %q, got nil", test.expectedErr)
|
||||||
if !strings.Contains(err.Error(), test.expectedErr) {
|
case err != nil && test.expectedErr == None:
|
||||||
t.Fatalf("expected %q, got %q", test.expectedErr, err.Error())
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
case err != nil && test.expectedErr != None:
|
||||||
|
if !strings.Contains(err.Error(), test.expectedErr) {
|
||||||
|
t.Fatalf("expected error %q, got %q", test.expectedErr, err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -100,11 +100,16 @@ EOF
|
|||||||
# wait with jsonpath without value to succeed
|
# wait with jsonpath without value to succeed
|
||||||
set +o errexit
|
set +o errexit
|
||||||
# Command: Wait with jsonpath without value
|
# Command: Wait with jsonpath without value
|
||||||
output_message=$(kubectl wait --for=jsonpath='{.status.replicas}' deploy/test-3 2>&1)
|
output_message_0=$(kubectl wait --for=jsonpath='{.status.replicas}' deploy/test-3 2>&1)
|
||||||
|
# Command: Wait with relaxed jsonpath and filter expression
|
||||||
|
output_message_1=$(kubectl wait \
|
||||||
|
--for='jsonpath=spec.template.spec.containers[?(@.name=="busybox")].image=busybox' \
|
||||||
|
deploy/test-3)
|
||||||
set -o errexit
|
set -o errexit
|
||||||
|
|
||||||
# Post-Condition: Wait succeed
|
# Post-Condition: Wait succeed
|
||||||
kube::test::if_has_string "${output_message}" 'deployment.apps/test-3 condition met'
|
kube::test::if_has_string "${output_message_0}" 'deployment.apps/test-3 condition met'
|
||||||
|
kube::test::if_has_string "${output_message_1}" 'deployment.apps/test-3 condition met'
|
||||||
|
|
||||||
# Clean deployment
|
# Clean deployment
|
||||||
kubectl delete deployment test-3
|
kubectl delete deployment test-3
|
||||||
|
Loading…
Reference in New Issue
Block a user