From 1b56edad0f2453fd9c0f360e046a7b64ce9e55dc Mon Sep 17 00:00:00 2001 From: Dennis Kniep Date: Thu, 19 Sep 2024 22:34:19 +0200 Subject: [PATCH] EnvoyPatchPolicy JsonPath docs & fixes (#4256) * fix: jsonPath escape chars & api docs (#4162) Signed-off-by: Dennis Kniep * fix: throw error if jsonPath returns no jsonPointers (#4162) Signed-off-by: Dennis Kniep * docs: JSONPath usage in EnvoyPatchPolicy (#4043) Signed-off-by: Dennis Kniep --------- Signed-off-by: Dennis Kniep --- api/v1alpha1/envoypatchpolicy_types.go | 12 +- ...eway.envoyproxy.io_envoypatchpolicies.yaml | 12 +- .../gateway.envoyproxy.io_envoyproxies.yaml | 12 +- internal/utils/jsonpatch/jsonpathtopointer.go | 18 ++- .../utils/jsonpatch/jsonpathtopointer_test.go | 151 +++++++++++++++--- internal/utils/jsonpatch/patch.go | 7 + internal/utils/jsonpatch/patch_test.go | 141 +++++++++++++++- .../jsonpatch-with-jsonpath-invalid.yaml | 68 ++++++++ internal/xds/translator/translator_test.go | 4 + site/content/en/latest/api/extension_types.md | 4 +- .../tasks/extensibility/envoy-patch-policy.md | 79 +++++++++ site/content/zh/latest/api/extension_types.md | 4 +- 12 files changed, 470 insertions(+), 42 deletions(-) create mode 100644 internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml diff --git a/api/v1alpha1/envoypatchpolicy_types.go b/api/v1alpha1/envoypatchpolicy_types.go index b23002e678f..a7ac8992dbc 100644 --- a/api/v1alpha1/envoypatchpolicy_types.go +++ b/api/v1alpha1/envoypatchpolicy_types.go @@ -109,12 +109,16 @@ type JSONPatchOperationType string type JSONPatchOperation struct { // Op is the type of operation to perform Op JSONPatchOperationType `json:"op"` - // Path is the location of the target document/field where the operation will be performed - // Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. + // Path is a JSONPointer expression. Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. + // It specifies the location of the target document/field where the operation will be performed // +optional Path *string `json:"path,omitempty"` - // JSONPath specifies the locations of the target document/field where the operation will be performed - // Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. + // JSONPath is a JSONPath expression. Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. + // It produces one or more JSONPointer expressions based on the given JSON document. + // If no JSONPointer is found, it will result in an error. + // If the 'Path' property is also set, it will be appended to the resulting JSONPointer expressions from the JSONPath evaluation. + // This is useful when creating a property that does not yet exist in the JSON document. + // The final JSONPointer expressions specifies the locations in the target document/field where the operation will be applied. // +optional JSONPath *string `json:"jsonPath,omitempty"` // From is the source location of the value to be copied or moved. Only valid diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml index d9729ab138c..591e61a4e53 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -73,8 +73,12 @@ spec: type: string jsonPath: description: |- - JSONPath specifies the locations of the target document/field where the operation will be performed - Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. + JSONPath is a JSONPath expression. Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. + It produces one or more JSONPointer expressions based on the given JSON document. + If no JSONPointer is found, it will result in an error. + If the 'Path' property is also set, it will be appended to the resulting JSONPointer expressions from the JSONPath evaluation. + This is useful when creating a property that does not yet exist in the JSON document. + The final JSONPointer expressions specifies the locations in the target document/field where the operation will be applied. type: string op: description: Op is the type of operation to perform @@ -88,8 +92,8 @@ spec: type: string path: description: |- - Path is the location of the target document/field where the operation will be performed - Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. + Path is a JSONPointer expression. Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. + It specifies the location of the target document/field where the operation will be performed type: string value: description: |- diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index deee471aa4f..145a3e4d41a 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -208,8 +208,12 @@ spec: type: string jsonPath: description: |- - JSONPath specifies the locations of the target document/field where the operation will be performed - Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. + JSONPath is a JSONPath expression. Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. + It produces one or more JSONPointer expressions based on the given JSON document. + If no JSONPointer is found, it will result in an error. + If the 'Path' property is also set, it will be appended to the resulting JSONPointer expressions from the JSONPath evaluation. + This is useful when creating a property that does not yet exist in the JSON document. + The final JSONPointer expressions specifies the locations in the target document/field where the operation will be applied. type: string op: description: Op is the type of operation to perform @@ -223,8 +227,8 @@ spec: type: string path: description: |- - Path is the location of the target document/field where the operation will be performed - Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. + Path is a JSONPointer expression. Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. + It specifies the location of the target document/field where the operation will be performed type: string value: description: |- diff --git a/internal/utils/jsonpatch/jsonpathtopointer.go b/internal/utils/jsonpatch/jsonpathtopointer.go index 730baa94ee2..18bfb569335 100644 --- a/internal/utils/jsonpatch/jsonpathtopointer.go +++ b/internal/utils/jsonpatch/jsonpathtopointer.go @@ -115,6 +115,20 @@ func nthToPointer(f jp.Nth) ([]byte, error) { return buf, nil } -func toPointer(f jp.Frag) ([]byte, error) { - return f.Append(nil, false, true), nil +func toPointer(f jp.Child) ([]byte, error) { + var buf []byte + + // JSONPointer escaping https://datatracker.ietf.org/doc/html/rfc6901#section-3 + for _, b := range []byte(string(f)) { + switch b { + case '~': + buf = append(buf, "~0"...) + case '/': + buf = append(buf, "~1"...) + default: + buf = append(buf, b) + } + } + + return buf, nil } diff --git a/internal/utils/jsonpatch/jsonpathtopointer_test.go b/internal/utils/jsonpatch/jsonpathtopointer_test.go index 4b57424562d..03bc25dd8c4 100644 --- a/internal/utils/jsonpatch/jsonpathtopointer_test.go +++ b/internal/utils/jsonpatch/jsonpathtopointer_test.go @@ -7,7 +7,6 @@ package jsonpatch import ( "sort" - "strconv" "testing" "github.com/ohler55/ojg/jp" @@ -87,8 +86,33 @@ const case3Route string = `{ "ignore_port_in_host_matching": true }` +const case4Escaping string = `{ + "values": [{ + "name": "test1", + "dotted.key": "Hello" + }, + { + "name": "test2", + "dotted.key": "there" + }, + { + "name": "test3", + "~abc": "tilde" + }, + { + "name": "test4", + "//abc": "slash" + }, + { + "name": "test5", + "~/abc/~": "mixed" + }] +}` + func Test(t *testing.T) { - tests := []struct { + testCases := []struct { + name string + // Json Document doc string @@ -102,6 +126,20 @@ func Test(t *testing.T) { expected []string }{ { + name: "TestCase-01", + doc: case1Simple, + jsonPath: "$.xyz", + expected: []string{}, + }, + { + name: "TestCase-02", + doc: case1Simple, + jsonPath: "$.xyz", + path: "doesnotexist", + expected: []string{}, + }, + { + name: "TestCase-03", doc: case1Simple, jsonPath: "$.a", expected: []string{ @@ -109,6 +147,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-04", doc: case2Nested, jsonPath: "$.v[?(@.x=='test2')]", expected: []string{ @@ -116,6 +155,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-05", doc: case2Nested, jsonPath: "..v[?(@.x=='test1')].y", expected: []string{ @@ -123,6 +163,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-06", doc: case2Nested, jsonPath: "$.v[?(@.x=='test2')].y", expected: []string{ @@ -130,6 +171,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-07", doc: case2Nested, jsonPath: "$.v[?(@.x=='test1')].y", expected: []string{ @@ -137,6 +179,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-08", doc: case2Nested, jsonPath: "$.v[*].y", expected: []string{ @@ -145,11 +188,13 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-09", doc: case2Nested, jsonPath: "$.v[?(@.x=='UNKNOWN')].y", expected: []string{}, }, { + name: "TestCase-10", doc: case1Simple, jsonPath: ".a", expected: []string{ @@ -157,6 +202,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-11", doc: case1Simple, jsonPath: "a", expected: []string{ @@ -164,6 +210,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-12", doc: case2Nested, jsonPath: "f.w", expected: []string{ @@ -171,6 +218,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-13", doc: case2Nested, jsonPath: "f.*", expected: []string{ @@ -180,6 +228,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-14", doc: case2Nested, jsonPath: "v.*", expected: []string{ @@ -188,6 +237,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-15", doc: case2Nested, jsonPath: "v.**", expected: []string{ @@ -198,6 +248,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-16", doc: case2Nested, jsonPath: "$..y", expected: []string{ @@ -208,6 +259,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-17", doc: case2Nested, jsonPath: "..y", expected: []string{ @@ -218,6 +270,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-18", doc: case2Nested, jsonPath: "**.y", expected: []string{ @@ -226,6 +279,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-19", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www_example_com')]", expected: []string{ @@ -233,6 +287,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-20", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www_test_com')]", expected: []string{ @@ -240,6 +295,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-21", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www')]", expected: []string{ @@ -248,6 +304,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-22", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www')].route.cluster", expected: []string{ @@ -256,6 +313,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-23", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www')]['route']['cluster']", expected: []string{ @@ -264,6 +322,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-24", doc: case3Route, jsonPath: "..routes[?(@.name=='httproute/default/backend/rule/1/match/1/www_example_com')].route.upgrade_configs", expected: []string{ @@ -271,6 +330,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-25", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www')]", path: "/abc", @@ -280,6 +340,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-26", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www')]", path: "abc", @@ -289,6 +350,7 @@ func Test(t *testing.T) { }, }, { + name: "TestCase-27", doc: case3Route, jsonPath: "..routes[?(@.name =~ 'www')]", path: "/", @@ -297,26 +359,68 @@ func Test(t *testing.T) { "/virtual_hosts/1/routes/0/", }, }, + { + name: "TestCase-28", + doc: case4Escaping, + jsonPath: "$.values[?(@.name =~ 'test2')]", + path: "dotted.key", + expected: []string{ + "/values/1/dotted.key", + }, + }, + { + name: "TestCase-29", + doc: case4Escaping, + jsonPath: "$.values[?(@.name =~ 'test2')]['dotted.key']", + expected: []string{ + "/values/1/dotted.key", + }, + }, + { + name: "TestCase-30", + doc: case4Escaping, + jsonPath: "$.values[?(@.name =~ 'test3')].~abc", + expected: []string{ + "/values/2/~0abc", + }, + }, + { + name: "TestCase-31", + doc: case4Escaping, + jsonPath: "$.values[?(@.name =~ 'test4')]['//abc']", + expected: []string{ + "/values/3/~1~1abc", + }, + }, + { + name: "TestCase-32", + doc: case4Escaping, + jsonPath: "$.values[?(@.name =~ 'test5')]['~/abc/~']", + expected: []string{ + "/values/4/~0~1abc~1~0", + }, + }, } - for i, test := range tests { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pointers, err := ConvertPathToPointers([]byte(tc.doc), tc.jsonPath, tc.path) + if err != nil { + require.NoError(t, err) + } - testCasePrefix := "TestCase " + strconv.Itoa(i+1) - pointers, err := ConvertPathToPointers([]byte(test.doc), test.jsonPath, test.path) - if err != nil { - t.Error(testCasePrefix + ": Error during conversion:\n" + err.Error()) - continue - } + expectedAsString := asString(tc.expected) + pointersAsString := asString(pointers) - expectedAsString := asString(test.expected) - pointersAsString := asString(pointers) - - require.Equal(t, expectedAsString, pointersAsString) + require.Equal(t, expectedAsString, pointersAsString) + }) } } func TestException(t *testing.T) { tests := []struct { + name string + // Json Document doc string @@ -330,32 +434,33 @@ func TestException(t *testing.T) { expected string }{ { + name: "TestCaseEx-01", doc: case1Simple, jsonPath: ".$", expected: "Error during parsing jpath", }, { + name: "TestCaseEx-02", doc: case1Simple, jsonPath: "$", expected: "only Root", }, { + name: "TestCaseEx-03", doc: "{", jsonPath: ".$", expected: "Error during parsing json", }, } - for i, test := range tests { - - testCasePrefix := "TestCase " + strconv.Itoa(i+1) - _, err := ConvertPathToPointers([]byte(test.doc), test.jsonPath, test.path) - if err == nil { - t.Error(testCasePrefix + ": Error expected, but no error found!") - continue - } - - require.ErrorContains(t, err, test.expected) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := ConvertPathToPointers([]byte(test.doc), test.jsonPath, test.path) + if err == nil { + require.Error(t, err) + } + require.ErrorContains(t, err, test.expected) + }) } } diff --git a/internal/utils/jsonpatch/patch.go b/internal/utils/jsonpatch/patch.go index 15cac85a308..8c14ae19f46 100644 --- a/internal/utils/jsonpatch/patch.go +++ b/internal/utils/jsonpatch/patch.go @@ -46,6 +46,13 @@ func ApplyJSONPatches(document json.RawMessage, patches ...ir.JSONPatchOperation tErrs = errors.Join(tErrs, tErr) continue } + if len(jsonPointers) == 0 { + tErr := fmt.Errorf("no jsonPointers were found while evaluating the jsonPath: '%s'. "+ + "Ensure the elements you are trying to select with the jsonPath exist in the document. "+ + "If you need to add a non-existing property, use the 'path' attribute", *p.JSONPath) + tErrs = errors.Join(tErrs, tErr) + continue + } } else { jsonPointers = []string{*p.Path} } diff --git a/internal/utils/jsonpatch/patch_test.go b/internal/utils/jsonpatch/patch_test.go index ace677124e0..dbdd63fc527 100644 --- a/internal/utils/jsonpatch/patch_test.go +++ b/internal/utils/jsonpatch/patch_test.go @@ -6,6 +6,7 @@ package jsonpatch import ( + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -40,14 +41,44 @@ const sourceDocument = ` } ` +const sourceDotEscape = ` + { + "otherLevel": { + "dot.key": "oldValue", + "~my": "file", + "/other/": "zip" + } + } +` + +var expectedDotEscapeCase1 = `{ + "otherLevel": { + "dot.key": "newValue", + "~my": "file", + "/other/": "zip" + } +}` + +var expectedDotEscapeCase2 = `{ + "otherLevel": { + "dot.key": "oldValue", + "~my": "folder", + "/other/": "tar" + } +}` + func TestApplyJSONPatches(t *testing.T) { testCases := []struct { + doc string name string patchOperation []ir.JSONPatchOperation errorExpected bool + errorContains *string + expectedDoc *string }{ { name: "simple add with single patch", + doc: sourceDocument, patchOperation: []ir.JSONPatchOperation{ { Op: "add", @@ -61,6 +92,7 @@ func TestApplyJSONPatches(t *testing.T) { }, { name: "two operations in a set", + doc: sourceDocument, patchOperation: []ir.JSONPatchOperation{ { Op: "add", @@ -78,6 +110,7 @@ func TestApplyJSONPatches(t *testing.T) { }, { name: "invalid operation", + doc: sourceDocument, patchOperation: []ir.JSONPatchOperation{ { Op: "badbadbad", @@ -88,9 +121,11 @@ func TestApplyJSONPatches(t *testing.T) { }, }, errorExpected: true, + errorContains: ptr.To("unsupported JSONPatch operation"), }, { name: "jsonpath affecting two places", + doc: sourceDocument, patchOperation: []ir.JSONPatchOperation{ { Op: "remove", @@ -101,6 +136,7 @@ func TestApplyJSONPatches(t *testing.T) { }, { name: "invalid jsonpath", + doc: sourceDocument, patchOperation: []ir.JSONPatchOperation{ { Op: "remove", @@ -108,17 +144,120 @@ func TestApplyJSONPatches(t *testing.T) { }, }, errorExpected: true, + errorContains: ptr.To("unable to convert jsonPath"), + }, + { + name: "dot escaped json path", + doc: sourceDotEscape, + patchOperation: []ir.JSONPatchOperation{ + { + Op: "replace", + JSONPath: ptr.To("$.otherLevel['dot.key']"), + Value: &apiextensionsv1.JSON{ + Raw: []byte("\"newValue\""), + }, + }, + }, + expectedDoc: &expectedDotEscapeCase1, + errorExpected: false, + }, + { + name: "dot escaped json path combined with path", + doc: sourceDotEscape, + patchOperation: []ir.JSONPatchOperation{ + { + Op: "replace", + Path: ptr.To("dot.key"), + JSONPath: ptr.To("$.otherLevel"), + Value: &apiextensionsv1.JSON{ + Raw: []byte("\"newValue\""), + }, + }, + }, + expectedDoc: &expectedDotEscapeCase1, + errorExpected: false, + }, + { + name: "json pointer chars which need to be escaped", + doc: sourceDotEscape, + patchOperation: []ir.JSONPatchOperation{ + { + Op: "replace", + JSONPath: ptr.To("$.otherLevel['~my']"), + Value: &apiextensionsv1.JSON{ + Raw: []byte("\"folder\""), + }, + }, + { + Op: "replace", + JSONPath: ptr.To("$.otherLevel['/other/']"), + Value: &apiextensionsv1.JSON{ + Raw: []byte("\"tar\""), + }, + }, + }, + expectedDoc: &expectedDotEscapeCase2, + errorExpected: false, + }, + { + name: "jsonPath returns no jsonPointer", + doc: sourceDocument, + patchOperation: []ir.JSONPatchOperation{ + { + Op: "replace", + JSONPath: ptr.To("$.secondLevel.doesNotExist"), + Value: &apiextensionsv1.JSON{ + Raw: []byte("\"folder\""), + }, + }, + }, + errorExpected: true, + errorContains: ptr.To("no jsonPointers were found"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := ApplyJSONPatches([]byte(sourceDocument), tc.patchOperation...) + jDoc, err := ApplyJSONPatches([]byte(tc.doc), tc.patchOperation...) if tc.errorExpected { require.Error(t, err) + if tc.errorContains != nil { + require.ErrorContains(t, err, *tc.errorContains) + } } else { + if tc.expectedDoc != nil { + resultData, err := jDoc.MarshalJSON() + if err != nil { + t.Error(err) + } + + resultJSON, err := formatJSON(resultData) + if err != nil { + t.Error(err) + } + + expectedJSON, err := formatJSON([]byte(*tc.expectedDoc)) + if err != nil { + t.Error(err) + } + + require.Equal(t, expectedJSON, resultJSON) + } require.NoError(t, err) } }) } } + +func formatJSON(s []byte) (string, error) { + var obj map[string]interface{} + err := json.Unmarshal(s, &obj) + if err != nil { + return "", err + } + buf, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return "", err + } + return string(buf), nil +} diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml new file mode 100644 index 00000000000..5b677788a22 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml @@ -0,0 +1,68 @@ +envoyPatchPolicies: +- status: + ancestors: + - ancestorRef: + group: "gateway.networking.k8s.io" + kind: "Gateway" + namespace: "default" + name: "foobar" + name: "first-policy" + namespace: "default" + jsonPatches: + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: "first-route-dest" + operation: + op: "replace" + jsonPath: "..doesNotExists" + value: "50" +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + tls: + alpnProtocols: + - h2 + - http/1.1 + certificates: + - name: secret-1 + # byte slice representation of "key-data" + serverCertificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] + # byte slice representation of "key-data" + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] + - name: secret-2 + serverCertificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] + routes: + - name: "first-route" + hostname: "*" + headerMatches: + - name: user + stringMatch: + exact: "jason" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "second-route" + hostname: "*" + headerMatches: + - name: user + stringMatch: + exact: "james" + - name: country + stringMatch: + exact: "US" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "4.5.6.7" + port: 60000 + diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 06a9a86131b..e939ffb2b8b 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -58,6 +58,10 @@ func TestTranslateXds(t *testing.T) { "jsonpatch-with-jsonpath": { requireEnvoyPatchPolicies: true, }, + "jsonpatch-with-jsonpath-invalid": { + requireEnvoyPatchPolicies: true, + errMsg: "no jsonPointers were found while evaluating the jsonPath", + }, "jsonpatch-add-op-empty-jsonpath": { requireEnvoyPatchPolicies: true, errMsg: "a patch operation must specify a path or jsonPath", diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 4599c3214c4..70b7608406e 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2186,8 +2186,8 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `op` | _[JSONPatchOperationType](#jsonpatchoperationtype)_ | true | Op is the type of operation to perform | -| `path` | _string_ | false | Path is the location of the target document/field where the operation will be performed
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | -| `jsonPath` | _string_ | false | JSONPath specifies the locations of the target document/field where the operation will be performed
Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. | +| `path` | _string_ | false | Path is a JSONPointer expression. Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details.
It specifies the location of the target document/field where the operation will be performed | +| `jsonPath` | _string_ | false | JSONPath is a JSONPath expression. Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details.
It produces one or more JSONPointer expressions based on the given JSON document.
If no JSONPointer is found, it will result in an error.
If the 'Path' property is also set, it will be appended to the resulting JSONPointer expressions from the JSONPath evaluation.
This is useful when creating a property that does not yet exist in the JSON document.
The final JSONPointer expressions specifies the locations in the target document/field where the operation will be applied. | | `from` | _string_ | false | From is the source location of the value to be copied or moved. Only valid
for move or copy operations
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | | `value` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | false | Value is the new value of the path location. The value is only used by
the `add` and `replace` operations. | diff --git a/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md b/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md index 36930d73785..e503244c503 100644 --- a/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md +++ b/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md @@ -274,6 +274,85 @@ Handling connection for 8888 could not find what you are looking for ``` +### Customize VirtualHost by name + +* Use EnvoyProxy's `include_attempt_count_in_response` feature to include the attempt count as header in the downstream response. +* Apply the configuration + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <// + name: default/eg/http + operation: + op: add + # Every virtual_host that ends with 'www_example_com' (using RegEx Filter) + jsonPath: "..virtual_hosts[?match(@.name, '.*www_example_com')]" + # If the property does not exists, it can not be selected with jsonPath + # Therefore the new property must be set in path + path: "include_attempt_count_in_response" + value: true +EOF +``` + +{{% /tab %}} +{{% tab header="Apply from file" %}} +Save and apply the following resource to your cluster: + +```yaml +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: include-attempts + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: eg + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" + # The RouteConfiguration name is of the form // + name: default/eg/http + operation: + op: add + # Every virtual_host that ends with 'www_example_com' (using RegEx Filter) + jsonPath: "..virtual_hosts[?match(@.name, '.*www_example_com')]" + # If the property does not exists, it can not be selected with jsonPath + # Therefore the new property must be set in path + path: "include_attempt_count_in_response" + value: true +``` + +{{% /tab %}} +{{< /tabpane >}} + +* Test it out by looking at the response headers + +``` +$ curl -v --header "Host: www.example.com" http://localhost:8888/ +... +< x-envoy-attempt-count: 1 +... +``` + ## Debugging ### Runtime diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 4599c3214c4..70b7608406e 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -2186,8 +2186,8 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `op` | _[JSONPatchOperationType](#jsonpatchoperationtype)_ | true | Op is the type of operation to perform | -| `path` | _string_ | false | Path is the location of the target document/field where the operation will be performed
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | -| `jsonPath` | _string_ | false | JSONPath specifies the locations of the target document/field where the operation will be performed
Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details. | +| `path` | _string_ | false | Path is a JSONPointer expression. Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details.
It specifies the location of the target document/field where the operation will be performed | +| `jsonPath` | _string_ | false | JSONPath is a JSONPath expression. Refer to https://datatracker.ietf.org/doc/rfc9535/ for more details.
It produces one or more JSONPointer expressions based on the given JSON document.
If no JSONPointer is found, it will result in an error.
If the 'Path' property is also set, it will be appended to the resulting JSONPointer expressions from the JSONPath evaluation.
This is useful when creating a property that does not yet exist in the JSON document.
The final JSONPointer expressions specifies the locations in the target document/field where the operation will be applied. | | `from` | _string_ | false | From is the source location of the value to be copied or moved. Only valid
for move or copy operations
Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. | | `value` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#json-v1-apiextensions-k8s-io)_ | false | Value is the new value of the path location. The value is only used by
the `add` and `replace` operations. |