Merge pull request #118748 from andreaskaris/kubectl-wait-for

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

Kubernetes-commit: d486180eb050c756d9add30377980eced146ffa1
This commit is contained in:
Kubernetes Publisher 2023-08-15 15:17:26 -07:00
commit 0266cec8bc
4 changed files with 132 additions and 33 deletions

14
go.mod
View File

@ -30,10 +30,10 @@ require (
github.com/stretchr/testify v1.8.2
golang.org/x/sys v0.10.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.0.0-20230807202504-d11dea4516ea
k8s.io/apimachinery v0.0.0-20230807201405-8071e5f05ff1
k8s.io/api v0.0.0-20230810042731-2f6eec10c476
k8s.io/apimachinery v0.0.0-20230815235016-14436eb53afd
k8s.io/cli-runtime v0.0.0-20230807221238-0daafa128c61
k8s.io/client-go v0.0.0-20230807204204-49410bfbbcf9
k8s.io/client-go v0.0.0-20230816000758-856e847bb7cb
k8s.io/component-base v0.0.0-20230807211050-31137ad9f7f2
k8s.io/component-helpers v0.0.0-20230807211335-91a729046f19
k8s.io/klog/v2 v2.100.1
@ -95,11 +95,11 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.0.0-20230807202504-d11dea4516ea
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230807201405-8071e5f05ff1
k8s.io/api => k8s.io/api v0.0.0-20230810042731-2f6eec10c476
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230815235016-14436eb53afd
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20230807221238-0daafa128c61
k8s.io/client-go => k8s.io/client-go v0.0.0-20230807204204-49410bfbbcf9
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20230807201020-acb52e329a7f
k8s.io/client-go => k8s.io/client-go v0.0.0-20230816000758-856e847bb7cb
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20230815234323-164b07cd93ea
k8s.io/component-base => k8s.io/component-base v0.0.0-20230807211050-31137ad9f7f2
k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20230807211335-91a729046f19
k8s.io/metrics => k8s.io/metrics v0.0.0-20230807220806-4c5f05109520

12
go.sum
View File

@ -273,14 +273,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20230807202504-d11dea4516ea h1:1IjoJXclbOUWuIkoxE1+p/7hQvNqXiTnAikqG+h+B54=
k8s.io/api v0.0.0-20230807202504-d11dea4516ea/go.mod h1:RFi7MZgMNqcWc0azfutkpPR/OdHWZjnTAwdFHTKjAUQ=
k8s.io/apimachinery v0.0.0-20230807201405-8071e5f05ff1 h1:NElIwKgvUTGJ2/PZEctXR9JmNzUbb8q38ojfC5guWeU=
k8s.io/apimachinery v0.0.0-20230807201405-8071e5f05ff1/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw=
k8s.io/api v0.0.0-20230810042731-2f6eec10c476 h1:1LpPoqkYurARQ/TQ0U3DRAclyCkM7hMCMCUVymCR3jM=
k8s.io/api v0.0.0-20230810042731-2f6eec10c476/go.mod h1:RFi7MZgMNqcWc0azfutkpPR/OdHWZjnTAwdFHTKjAUQ=
k8s.io/apimachinery v0.0.0-20230815235016-14436eb53afd h1:x//MctFnLnU7WIEUEorfJtI5RkFptZADuNKPFxZBdbg=
k8s.io/apimachinery v0.0.0-20230815235016-14436eb53afd/go.mod h1:X0xh/chESs2hP9koe+SdIAcXWcQ+RM5hy0ZynB+yEvw=
k8s.io/cli-runtime v0.0.0-20230807221238-0daafa128c61 h1:rucHwIDQTB+CBn5R+vYaAaLWUK+KKXPPPexdFng0PHY=
k8s.io/cli-runtime v0.0.0-20230807221238-0daafa128c61/go.mod h1:2TfvpatNccVWjkFyiHv65ekELznAVhjJ34PaFcSb/Vc=
k8s.io/client-go v0.0.0-20230807204204-49410bfbbcf9 h1:/wXosf3IlbuvLQQBDkBY+lSOmJGaTB5931MVjn3Zq5A=
k8s.io/client-go v0.0.0-20230807204204-49410bfbbcf9/go.mod h1:NbnWu9sTxFULr211okLOk4jiLYAyWn+kFzXmCLu2pK4=
k8s.io/client-go v0.0.0-20230816000758-856e847bb7cb h1:QLY5cHaZwawHP6394w6miAQAaJ2fwlA/TfRHXmG4U3M=
k8s.io/client-go v0.0.0-20230816000758-856e847bb7cb/go.mod h1:xt/XQN6z9voSDnQ/uCIJ3a5n5sk2lhhnwzWGeuDhsvE=
k8s.io/component-base v0.0.0-20230807211050-31137ad9f7f2 h1:bgkLpsQhIRm8Rd6h9V/n50sN63k6sEzX+Q8nCpZCCX4=
k8s.io/component-base v0.0.0-20230807211050-31137ad9f7f2/go.mod h1:wjy+fowSTnR9NfN23CZuwDq+yF+viZTN5nbGbXcOYBM=
k8s.io/component-helpers v0.0.0-20230807211335-91a729046f19 h1:WTPiAU5fRLmytj73dy6Ew2voeWwN/O9yfGHzdmSds1s=

View File

@ -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
@ -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
}
@ -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}'")
}
@ -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

View File

@ -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())
}
}
})
}