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. |