Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubectl: Improve conditionFuncFor expression parsing for wait --for jsonpath #118748

Merged
merged 1 commit into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 30 additions & 4 deletions staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ var (
# Wait for the pod "busybox1" to contain the status phase to be "Running"
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.
kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer

Expand Down Expand Up @@ -209,8 +212,8 @@ func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error)
}.IsConditionMet, nil
}
if strings.HasPrefix(condition, "jsonpath=") {
splitStr := strings.Split(condition, "=")
jsonPathExp, jsonPathValue, err := processJSONPathInput(splitStr[1:])
jsonPathInput := strings.TrimPrefix(condition, "jsonpath=")
jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -241,8 +244,10 @@ func newJSONPathParser(jsonPathExpression string) (*jsonpath.JSONPath, error) {
return j, nil
}

// processJSONPathInput will parses the user's JSONPath input containing JSON expression and, optionally, JSON value for matching condition and process it
func processJSONPathInput(jsonPathInput []string) (string, string, error) {
// processJSONPathInput will parse and process the provided JSONPath input containing a JSON expression and optionally
// a value for the matching condition.
func processJSONPathInput(input string) (string, string, error) {
jsonPathInput := splitJSONPathInput(input)
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}'")
}
Expand All @@ -260,6 +265,27 @@ func processJSONPathInput(jsonPathInput []string) (string, string, error) {
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
type ResourceLocation struct {
GroupResource schema.GroupResource
Expand Down
105 changes: 89 additions & 16 deletions staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1534,39 +1534,112 @@ 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) {
// TestConditionFuncFor tests that the condition string can be properly parsed into a ConditionFunc.
func TestConditionFuncFor(t *testing.T) {
tests := []struct {
name string
condition string
expectedResult JSONPathWait
expectedErr string
name string
condition string
expectedErr string
}{
{
name: "missing JSONPath expression",
name: "jsonpath missing JSONPath expression",
condition: "jsonpath=",
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'",
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}=",
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 {
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())
switch {
case err == nil && test.expectedErr != None:
t.Fatalf("expected error %q, got nil", test.expectedErr)
case err != nil && test.expectedErr == None:
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())
}
}
})
}
Expand Down
9 changes: 7 additions & 2 deletions test/cmd/wait.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,16 @@ EOF
# wait with jsonpath without value to succeed
set +o errexit
# 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

# 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
kubectl delete deployment test-3
Expand Down