From 72b811303a0a4aac84b5c15aab14459475919e7e Mon Sep 17 00:00:00 2001 From: Dennis Kniep Date: Fri, 5 Jul 2024 00:22:23 +0200 Subject: [PATCH] feat(translator): JsonPath in PatchPolicy JsonPath can be utilized to select elements for JSONPatch in EnvoyPatchPolicy Signed-off-by: Dennis Kniep --- api/v1alpha1/envoypatchpolicy_types.go | 7 +- api/v1alpha1/zz_generated.deepcopy.go | 10 + ...eway.envoyproxy.io_envoypatchpolicies.yaml | 6 +- go.mod | 1 + go.sum | 2 + internal/gatewayapi/envoypatchpolicy.go | 1 + internal/ir/xds.go | 19 +- internal/ir/zz_generated.deepcopy.go | 10 + internal/xds/translator/jsonpatch.go | 233 ++++++----- internal/xds/translator/jsonpathtopointer.go | 120 ++++++ .../xds/translator/jsonpathtopointer_test.go | 395 ++++++++++++++++++ .../jsonpatch-add-op-empty-jsonpath.yaml | 63 +++ .../in/xds-ir/jsonpatch-with-jsonpath.yaml | 172 ++++++++ ...npatch-add-op-empty-jsonpath.clusters.yaml | 17 + ...patch-add-op-empty-jsonpath.endpoints.yaml | 24 ++ ...-op-empty-jsonpath.envoypatchpolicies.yaml | 16 + ...patch-add-op-empty-jsonpath.listeners.yaml | 52 +++ ...sonpatch-add-op-empty-jsonpath.routes.yaml | 18 + ...onpatch-add-op-empty-jsonpath.secrets.yaml | 12 + .../jsonpatch-with-jsonpath.clusters.yaml | 47 +++ .../jsonpatch-with-jsonpath.endpoints.yaml | 32 ++ ...atch-with-jsonpath.envoypatchpolicies.yaml | 16 + .../jsonpatch-with-jsonpath.listeners.yaml | 64 +++ .../jsonpatch-with-jsonpath.routes.yaml | 36 ++ .../jsonpatch-with-jsonpath.secrets.yaml | 16 + internal/xds/translator/translator_test.go | 6 + site/content/en/latest/api/extension_types.md | 3 +- site/content/zh/latest/api/extension_types.md | 3 +- 28 files changed, 1292 insertions(+), 109 deletions(-) create mode 100644 internal/xds/translator/jsonpathtopointer.go create mode 100644 internal/xds/translator/jsonpathtopointer_test.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.envoypatchpolicies.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.secrets.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.envoypatchpolicies.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.secrets.yaml diff --git a/api/v1alpha1/envoypatchpolicy_types.go b/api/v1alpha1/envoypatchpolicy_types.go index 22effb69756..b23002e678f 100644 --- a/api/v1alpha1/envoypatchpolicy_types.go +++ b/api/v1alpha1/envoypatchpolicy_types.go @@ -111,7 +111,12 @@ type JSONPatchOperation struct { 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 string `json:"path"` + // +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. + // +optional + JSONPath *string `json:"jsonPath,omitempty"` // 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. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c8f7a5711fa..0897eaacdb2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2815,6 +2815,16 @@ func (in *ImageWasmCodeSource) DeepCopy() *ImageWasmCodeSource { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JSONPatchOperation) DeepCopyInto(out *JSONPatchOperation) { *out = *in + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } + if in.JSONPath != nil { + in, out := &in.JSONPath, &out.JSONPath + *out = new(string) + **out = **in + } if in.From != nil { in, out := &in.From, &out.From *out = new(string) 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 e385b0d4bb0..f57a644066f 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -71,6 +71,11 @@ spec: for move or copy operations Refer to https://datatracker.ietf.org/doc/html/rfc6901 for more details. 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. + type: string op: description: Op is the type of operation to perform enum: @@ -93,7 +98,6 @@ spec: x-kubernetes-preserve-unknown-fields: true required: - op - - path type: object type: description: Type is the typed URL of the Envoy xDS Resource diff --git a/go.mod b/go.mod index e66adb8a3db..0577dd21ddc 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/grafana/tempo v1.5.0 github.com/hashicorp/go-multierror v1.1.1 github.com/miekg/dns v1.1.61 + github.com/ohler55/ojg v1.22.1 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/common v0.55.0 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index ed3fe16fab6..84aab33dffd 100644 --- a/go.sum +++ b/go.sum @@ -899,6 +899,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/ohler55/ojg v1.22.1 h1:MvUieaWTwksoYk47GYyP9kzXIAkxHYX6rxeLjUEeq/8= +github.com/ohler55/ojg v1.22.1/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/internal/gatewayapi/envoypatchpolicy.go b/internal/gatewayapi/envoypatchpolicy.go index 9ea9102ac76..5d2480f5d23 100644 --- a/internal/gatewayapi/envoypatchpolicy.go +++ b/internal/gatewayapi/envoypatchpolicy.go @@ -114,6 +114,7 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo irPatch.Name = patch.Name irPatch.Operation.Op = string(patch.Operation.Op) irPatch.Operation.Path = patch.Operation.Path + irPatch.Operation.JSONPath = patch.Operation.JSONPath irPatch.Operation.From = patch.Operation.From irPatch.Operation.Value = patch.Operation.Value diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 5e9cb6ac40e..a249905411b 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -25,6 +25,10 @@ import ( egv1a1validation "github.com/envoyproxy/gateway/api/v1alpha1/validation" ) +const ( + EmptyPath = "" +) + var ( ErrListenerNameEmpty = errors.New("field Name must be specified") ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") @@ -1720,7 +1724,12 @@ type JSONPatchOperation struct { Op string `json:"op" yaml:"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 string `json:"path" yaml:"path"` + // +optional + Path *string `json:"path,omitempty" yaml:"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. + // +optional + JSONPath *string `json:"jsonPath,omitempty" yaml:"jsonPath,omitempty"` // 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. @@ -1730,6 +1739,14 @@ type JSONPatchOperation struct { Value *apiextensionsv1.JSON `json:"value,omitempty" yaml:"value,omitempty"` } +func (o *JSONPatchOperation) IsPathNilOrEmpty() bool { + return o.Path == nil || *o.Path == EmptyPath +} + +func (o *JSONPatchOperation) IsJSONPathNilOrEmpty() bool { + return o.JSONPath == nil || *o.JSONPath == EmptyPath +} + // Tracing defines the configuration for tracing a Envoy xDS Resource // +k8s:deepcopy-gen=true type Tracing struct { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 7d453c5f522..aba1b3aba6e 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -1595,6 +1595,16 @@ func (in *JSONPatchConfig) DeepCopy() *JSONPatchConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JSONPatchOperation) DeepCopyInto(out *JSONPatchOperation) { *out = *in + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } + if in.JSONPath != nil { + in, out := &in.JSONPath, &out.JSONPath + *out = new(string) + **out = **in + } if in.From != nil { in, out := &in.From, &out.From *out = new(string) diff --git a/internal/xds/translator/jsonpatch.go b/internal/xds/translator/jsonpatch.go index e7808abe0c5..83db9103b06 100644 --- a/internal/xds/translator/jsonpatch.go +++ b/internal/xds/translator/jsonpatch.go @@ -84,9 +84,9 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []* } } - // If Path is "" and op is "add", unmarshal and add the patch as a complete + // If Path and JSONPath is "" and op is "add", unmarshal and add the patch as a complete // resource - if p.Operation.Op == AddOperation && p.Operation.Path == EmptyPath { + if p.Operation.Op == AddOperation && p.Operation.IsPathNilOrEmpty() && p.Operation.IsJSONPathNilOrEmpty() { // Convert patch to JSON // The patch library expects an array so convert it into one y, err := yaml.Marshal(p.Operation.Value) @@ -240,125 +240,150 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []* } } - // Convert patch to JSON - // The patch library expects an array so convert it into one - y, err := yaml.Marshal([]ir.JSONPatchOperation{p.Operation}) - if err != nil { - tErr := fmt.Errorf("unable to marshal patch %+v, err: %s", p.Operation, err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - jsonBytes, err := yaml.YAMLToJSON(y) - if err != nil { - tErr := fmt.Errorf("unable to convert patch to json %s, err: %s", string(y), err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - patchObj, err := jsonpatchv5.DecodePatch(jsonBytes) - if err != nil { - tErr := fmt.Errorf("unable to decode patch %s, err: %s", string(jsonBytes), err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - - // Apply patch - opts := jsonpatchv5.NewApplyOptions() - opts.EnsurePathExistsOnAdd = true - modifiedJSON, err := patchObj.ApplyWithOptions(resourceJSON, opts) - if err != nil { - tErr := fmt.Errorf("unable to apply patch:\n%s on resource:\n%s, err: %s", string(jsonBytes), string(resourceJSON), err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - - // Unmarshal back to typed resource - // Use a temp staging variable that can be marshalled - // into and validated before saving it into the xds output resource - switch p.Type { - case resourcev3.ListenerType: - temp := &listenerv3.Listener{} - if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { - tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) - tErrs = errors.Join(tErrs, tErr) - continue + var jsonPointers []string + if p.Operation.JSONPath != nil { + path := "" + if p.Operation.Path != nil { + path = *p.Operation.Path } - if err = temp.Validate(); err != nil { - tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - if err = deepCopyPtr(temp, listener); err != nil { - tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) - tErrs = errors.Join(tErrs, tErr) - continue - } - case resourcev3.RouteType: - temp := &routev3.RouteConfiguration{} - if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { - tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) - tErrs = errors.Join(tErrs, tErr) - continue - } - if err = temp.Validate(); err != nil { - tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - if err = deepCopyPtr(temp, routeConfig); err != nil { - tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) - tErrs = errors.Join(tErrs, tErr) - continue - } - case resourcev3.ClusterType: - temp := &clusterv3.Cluster{} - if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { - tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) - tErrs = errors.Join(tErrs, tErr) - continue - } - if err = temp.Validate(); err != nil { - tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) - tErrs = errors.Join(tErrs, tErr) - continue - } - if err = deepCopyPtr(temp, cluster); err != nil { - tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + jsonPointers, err = ConvertPathToPointers(resourceJSON, *p.Operation.JSONPath, path) + if err != nil { + tErr := fmt.Errorf("unable to convert jsonPath: '%s' into jsonPointers, err: %s", *p.Operation.JSONPath, err.Error()) tErrs = errors.Join(tErrs, tErr) continue } - case resourcev3.EndpointType: - temp := &endpointv3.ClusterLoadAssignment{} - if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { - tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) - tErrs = errors.Join(tErrs, tErr) - continue + } else { + jsonPointers = []string{*p.Operation.Path} + } + + for _, path := range jsonPointers { + op := ir.JSONPatchOperation{ + Path: &path, + Op: p.Operation.Op, + Value: p.Operation.Value, + From: p.Operation.From, } - if err = temp.Validate(); err != nil { - tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + + // Convert patch to JSON + // The patch library expects an array so convert it into one + y, err := yaml.Marshal([]ir.JSONPatchOperation{op}) + if err != nil { + tErr := fmt.Errorf("unable to marshal patch %+v, err: %s", op, err.Error()) tErrs = errors.Join(tErrs, tErr) continue } - if err = deepCopyPtr(temp, endpoint); err != nil { - tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + jsonBytes, err := yaml.YAMLToJSON(y) + if err != nil { + tErr := fmt.Errorf("unable to convert patch to json %s, err: %s", string(y), err.Error()) tErrs = errors.Join(tErrs, tErr) continue } - case resourcev3.SecretType: - temp := &tlsv3.Secret{} - if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { - tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) + patchObj, err := jsonpatchv5.DecodePatch(jsonBytes) + if err != nil { + tErr := fmt.Errorf("unable to decode patch %s, err: %s", string(jsonBytes), err.Error()) tErrs = errors.Join(tErrs, tErr) continue } - if err = temp.Validate(); err != nil { - tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + + // Apply patch + opts := jsonpatchv5.NewApplyOptions() + opts.EnsurePathExistsOnAdd = true + modifiedJSON, err := patchObj.ApplyWithOptions(resourceJSON, opts) + if err != nil { + tErr := fmt.Errorf("unable to apply patch:\n%s on resource:\n%s, err: %s", string(jsonBytes), string(resourceJSON), err.Error()) tErrs = errors.Join(tErrs, tErr) continue } - if err = deepCopyPtr(temp, secret); err != nil { - tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) - tErrs = errors.Join(tErrs, tErr) - continue + + // Unmarshal back to typed resource + // Use a temp staging variable that can be marshalled + // into and validated before saving it into the xds output resource + switch p.Type { + case resourcev3.ListenerType: + temp := &listenerv3.Listener{} + if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { + tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = temp.Validate(); err != nil { + tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = deepCopyPtr(temp, listener); err != nil { + tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + tErrs = errors.Join(tErrs, tErr) + continue + } + case resourcev3.RouteType: + temp := &routev3.RouteConfiguration{} + if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { + tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = temp.Validate(); err != nil { + tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = deepCopyPtr(temp, routeConfig); err != nil { + tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + tErrs = errors.Join(tErrs, tErr) + continue + } + case resourcev3.ClusterType: + temp := &clusterv3.Cluster{} + if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { + tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = temp.Validate(); err != nil { + tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = deepCopyPtr(temp, cluster); err != nil { + tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + tErrs = errors.Join(tErrs, tErr) + continue + } + case resourcev3.EndpointType: + temp := &endpointv3.ClusterLoadAssignment{} + if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { + tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = temp.Validate(); err != nil { + tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = deepCopyPtr(temp, endpoint); err != nil { + tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + tErrs = errors.Join(tErrs, tErr) + continue + } + case resourcev3.SecretType: + temp := &tlsv3.Secret{} + if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { + tErr := fmt.Errorf(unmarshalErrorMessage(err, string(modifiedJSON))) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = temp.Validate(); err != nil { + tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", string(modifiedJSON), err.Error()) + tErrs = errors.Join(tErrs, tErr) + continue + } + if err = deepCopyPtr(temp, secret); err != nil { + tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", string(modifiedJSON), err) + tErrs = errors.Join(tErrs, tErr) + continue + } } } } diff --git a/internal/xds/translator/jsonpathtopointer.go b/internal/xds/translator/jsonpathtopointer.go new file mode 100644 index 00000000000..89d7fdf1c77 --- /dev/null +++ b/internal/xds/translator/jsonpathtopointer.go @@ -0,0 +1,120 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "reflect" + "strings" + + "github.com/ohler55/ojg/jp" + "github.com/ohler55/ojg/oj" + "github.com/pkg/errors" +) + +func ConvertPathToPointers(jsonDoc []byte, jsonPath string, path string) ([]string, error) { + var jsonPointers []string + + jObj, err := oj.Parse(jsonDoc) + if err != nil { + return nil, errors.Wrap(err, "Error during parsing json") + } + + jPath, err := jp.ParseString(jsonPath) + if err != nil { + return nil, errors.Wrap(err, "Error during parsing jpath") + } + + if len(jPath) == 1 { + _, isRoot := jPath[0].(jp.Root) + if isRoot { + return nil, errors.New("Using only Root ('$') in json path expression is not allowed!") + } + } + + locations := jPath.Locate(jObj, 0) + for _, l := range locations { + jsonPointer, err := expToPointer(l) + if err != nil { + return nil, errors.Wrap(err, "Error during converting path to pointer") + } + jsonPointers = append(jsonPointers, concat(jsonPointer, path)) + } + return jsonPointers, nil +} + +func concat(jsonPointer string, path string) string { + if path == "" { + return jsonPointer + } + const separator string = "/" + parts := []string{ + strings.TrimSuffix(jsonPointer, separator), + strings.TrimPrefix(path, separator), + } + return strings.Join(parts, separator) +} + +func expToPointer(e jp.Expr) (string, error) { + var buf []byte + for _, f := range e { + v, err := fragToPointer(f) + if err != nil { + return "", err + } + if v != nil { + buf = append(buf, '/') + } + + buf = append(buf, v...) + } + + return string(buf), nil +} + +func fragToPointer(f jp.Frag) ([]byte, error) { + switch v := f.(type) { + case jp.Root: + return rootToPointer() + case jp.Nth: + return nthToPointer(v) + case jp.Child: + return toPointer(v) + default: + return nil, errors.New("There is no conversion implemented for " + reflect.TypeOf(v).Name()) + } +} + +func rootToPointer() ([]byte, error) { + return nil, nil +} + +func nthToPointer(f jp.Nth) ([]byte, error) { + var buf []byte + i := int(f) + if i < 0 { + buf = append(buf, '-') + i = -i + } + num := [20]byte{} + cnt := 0 + for ; i != 0; cnt++ { + num[cnt] = byte(i%10) + '0' + i /= 10 + } + if 0 < cnt { + cnt-- + for ; 0 <= cnt; cnt-- { + buf = append(buf, num[cnt]) + } + } else { + buf = append(buf, '0') + } + return buf, nil +} + +func toPointer(f jp.Frag) ([]byte, error) { + return f.Append(nil, false, true), nil +} diff --git a/internal/xds/translator/jsonpathtopointer_test.go b/internal/xds/translator/jsonpathtopointer_test.go new file mode 100644 index 00000000000..cefb5925869 --- /dev/null +++ b/internal/xds/translator/jsonpathtopointer_test.go @@ -0,0 +1,395 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "sort" + "strconv" + "testing" + + "github.com/ohler55/ojg/jp" + "github.com/stretchr/testify/require" +) + +const case1Simple string = `{ + "a": "b" + }` + +const case2Nested string = `{ + "a": "b", + "v": [{ + "x": "test1", + "y": "hello" + }, + { + "x": "test2", + "y": "world" + }], + "f":{ + "w": "hi", + "q": "welcome", + "y": "ciao" + }, + "y": "c" + }` + +const case3Route string = `{ + "name": "default/eg/http", + "virtual_hosts": [ + { + "name": "default/eg/http/www_test_com", + "domains": [ + "www.test.com" + ], + "routes": [ + { + "name": "httproute/default/backend/rule/0/match/0/www_test_com", + "match": { + "prefix": "/" + }, + "route": { + "cluster": "httproute/default/backend/rule/0", + "upgrade_configs": [ + { + "upgrade_type": "websocket" + } + ] + } + } + ] + }, + { + "name": "default/eg/http/www_example_com", + "domains": [ + "www.example.com" + ], + "routes": [ + { + "name": "httproute/default/backend/rule/1/match/1/www_example_com", + "match": { + "prefix": "/" + }, + "route": { + "cluster": "httproute/default/backend/rule/1", + "upgrade_configs": [ + { + "upgrade_type": "websocket" + } + ] + } + } + ] + } + ], + "ignore_port_in_host_matching": true +}` + +func Test(t *testing.T) { + tests := []struct { + // Json Document + doc string + + // JSONPath + jsonPath string + + // path + path string + + // List of expected pointers + expected []string + }{ + { + doc: case1Simple, + jsonPath: "$.a", + expected: []string{ + "/a", + }, + }, + { + doc: case2Nested, + jsonPath: "$.v[?(@.x=='test2')]", + expected: []string{ + "/v/1", + }, + }, + { + doc: case2Nested, + jsonPath: "..v[?(@.x=='test1')].y", + expected: []string{ + "/v/0/y", + }, + }, + { + doc: case2Nested, + jsonPath: "$.v[?(@.x=='test2')].y", + expected: []string{ + "/v/1/y", + }, + }, + { + doc: case2Nested, + jsonPath: "$.v[?(@.x=='test1')].y", + expected: []string{ + "/v/0/y", + }, + }, + { + doc: case2Nested, + jsonPath: "$.v[*].y", + expected: []string{ + "/v/0/y", + "/v/1/y", + }, + }, + { + doc: case2Nested, + jsonPath: "$.v[?(@.x=='UNKNOWN')].y", + expected: []string{}, + }, + { + doc: case1Simple, + jsonPath: ".a", + expected: []string{ + "/a", + }, + }, + { + doc: case1Simple, + jsonPath: "a", + expected: []string{ + "/a", + }, + }, + { + doc: case2Nested, + jsonPath: "f.w", + expected: []string{ + "/f/w", + }, + }, + { + doc: case2Nested, + jsonPath: "f.*", + expected: []string{ + "/f/w", + "/f/q", + "/f/y", + }, + }, + { + doc: case2Nested, + jsonPath: "v.*", + expected: []string{ + "/v/0", + "/v/1", + }, + }, + { + doc: case2Nested, + jsonPath: "v.**", + expected: []string{ + "/v/0/x", + "/v/0/y", + "/v/1/x", + "/v/1/y", + }, + }, + { + doc: case2Nested, + jsonPath: "$..y", + expected: []string{ + "/f/y", + "/v/0/y", + "/v/1/y", + "/y", + }, + }, + { + doc: case2Nested, + jsonPath: "..y", + expected: []string{ + "/f/y", + "/v/0/y", + "/v/1/y", + "/y", + }, + }, + { + doc: case2Nested, + jsonPath: "**.y", + expected: []string{ + "/v/0/y", + "/v/1/y", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www_example_com')]", + expected: []string{ + "/virtual_hosts/1/routes/0", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www_test_com')]", + expected: []string{ + "/virtual_hosts/0/routes/0", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www')]", + expected: []string{ + "/virtual_hosts/0/routes/0", + "/virtual_hosts/1/routes/0", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www')].route.cluster", + expected: []string{ + "/virtual_hosts/0/routes/0/route/cluster", + "/virtual_hosts/1/routes/0/route/cluster", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www')]['route']['cluster']", + expected: []string{ + "/virtual_hosts/0/routes/0/route/cluster", + "/virtual_hosts/1/routes/0/route/cluster", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name=='httproute/default/backend/rule/1/match/1/www_example_com')].route.upgrade_configs", + expected: []string{ + "/virtual_hosts/1/routes/0/route/upgrade_configs", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www')]", + path: "/abc", + expected: []string{ + "/virtual_hosts/0/routes/0/abc", + "/virtual_hosts/1/routes/0/abc", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www')]", + path: "abc", + expected: []string{ + "/virtual_hosts/0/routes/0/abc", + "/virtual_hosts/1/routes/0/abc", + }, + }, + { + doc: case3Route, + jsonPath: "..routes[?(@.name =~ 'www')]", + path: "/", + expected: []string{ + "/virtual_hosts/0/routes/0/", + "/virtual_hosts/1/routes/0/", + }, + }, + } + + for i, test := range tests { + + 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(test.expected) + pointersAsString := asString(pointers) + + require.Equal(t, expectedAsString, pointersAsString) + } +} + +func TestException(t *testing.T) { + tests := []struct { + // Json Document + doc string + + // JSONPath + jsonPath string + + // path + path string + + // expected exception + expected string + }{ + { + doc: case1Simple, + jsonPath: ".$", + expected: "Error during parsing jpath", + }, + { + doc: case1Simple, + jsonPath: "$", + expected: "only Root", + }, + { + 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) + } +} + +func TestUnexpectedFrag(t *testing.T) { + expr := jp.Expr{} + expr = append(expr, jp.Union{}) + + _, err := expToPointer(expr) + if err == nil { + t.Error("Error expected, but no error found!") + } + + require.ErrorContains(t, err, "There is no conversion implemented for Union") +} + +func TestNegativeNth(t *testing.T) { + result, err := nthToPointer(jp.Nth(-1)) + if err != nil { + t.Error(err) + } + test := string(result) + if test != "-1" { + t.Error("expected -1, but was " + test + "!") + } +} + +func asString(values []string) string { + var buf []byte + + sort.Strings(values) + for _, v := range values { + buf = append(buf, []byte(v)...) + buf = append(buf, []byte("\n")...) + } + + return string(buf) +} diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml new file mode 100644 index 00000000000..9c248772920 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml @@ -0,0 +1,63 @@ +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: second-listener + operation: + op: add + value: + clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 4.5.6.7 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 +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 diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml new file mode 100644 index 00000000000..a02cad99d67 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml @@ -0,0 +1,172 @@ +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.listener.v3.Listener" + name: first-listener + operation: + op: "add" + jsonPath: "$.filter_chains[0].filters[0].typed_config" + path: "/preserve_external_request_id" + value: true + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: "first-listener" + operation: + op: "add" + jsonPath: "filter_chains[0].filters[0].typed_config.http_filters[0]" + value: + name: "envoy.filters.http.ratelimit" + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit" + domain: "eg-ratelimit" + failure_mode_deny: true + timeout: 1s + rate_limit_service: + grpc_service: + envoy_grpc: + cluster_name: rate-limit-cluster + transport_api_version: V3 + - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" + name: "first-listener" + operation: + op: "add" + jsonPath: "virtual_hosts[0]" + path: "rate_limits" + value: + - actions: + - remote_address: {} + - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" + name: "first-listener" + operation: + op: "replace" + jsonPath: "..routes[?(@.name=='second-route')].route.upgrade_configs" + value: + - upgrade_type: CONNECT + connect_config: + {} + - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" + name: rate-limit-cluster + operation: + op: add + path: "" + value: + name: rate-limit-cluster + type: STRICT_DNS + connect_timeout: 10s + lb_policy: ROUND_ROBIN + http2_protocol_options: {} + load_assignment: + cluster_name: rate-limit-cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ratelimit.svc.cluster.local + port_value: 8081 + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: "first-route-dest" + operation: + op: "replace" + jsonPath: "..endpoints[*].load_balancing_weight" + value: "50" + - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" + name: "secret-1" + operation: + op: "replace" + jsonPath: "$.tls_certificate.certificate_chain.inline_bytes" + value: "a2V5LWRhdGE=" + - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" + name: "test-secret" + operation: + op: "add" + path: "" + value: + name: test_secret + tls_certificate: + certificate_chain: + inline_bytes: Y2VydC1kYXRh + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: "first-route-dest" + operation: + op: add + jsonPath: "endpoints" + path: "/1" + value: + lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: "first-route-dest" + operation: + op: "move" + from: "/endpoints/0/load_balancing_weight" + path: "/endpoints/1/load_balancing_weight" + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: "first-route-dest" + operation: + op: copy + from: "/endpoints/1/load_balancing_weight" + path: "/endpoints/0/load_balancing_weight" +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/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.clusters.yaml new file mode 100644 index 00000000000..d53a7a1b2ce --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.clusters.yaml @@ -0,0 +1,17 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.endpoints.yaml new file mode 100644 index 00000000000..9a6f5a46c91 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.endpoints.yaml @@ -0,0 +1,24 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 4.5.6.7 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.envoypatchpolicies.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.envoypatchpolicies.yaml new file mode 100644 index 00000000000..9508dd3e7b3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.envoypatchpolicies.yaml @@ -0,0 +1,16 @@ +- name: first-policy + namespace: default + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: foobar + namespace: default + conditions: + - lastTransitionTime: null + message: Patches have been successfully applied. + reason: Programmed + status: "True" + type: Programmed + controllerName: "" diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.listeners.yaml new file mode 100644 index 00000000000..51c022c26f3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.listeners.yaml @@ -0,0 +1,52 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + drainType: MODIFY_ONLY + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: https-10080 + useRemoteAddress: true + name: first-listener + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + alpnProtocols: + - h2 + - http/1.1 + tlsCertificateSdsSecretConfigs: + - name: secret-1 + sdsConfig: + ads: {} + resourceApiVersion: V3 + - name: secret-2 + sdsConfig: + ads: {} + resourceApiVersion: V3 + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.routes.yaml new file mode 100644 index 00000000000..4a412b3576a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.routes.yaml @@ -0,0 +1,18 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + headers: + - name: user + stringMatch: + exact: jason + prefix: / + name: first-route + route: + cluster: first-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.secrets.yaml new file mode 100644 index 00000000000..ad88ffe43cd --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-add-op-empty-jsonpath.secrets.yaml @@ -0,0 +1,12 @@ +- name: secret-1 + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: secret-2 + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.clusters.yaml new file mode 100644 index 00000000000..b3842b6e52e --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.clusters.yaml @@ -0,0 +1,47 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + lbPolicy: LEAST_REQUEST + name: second-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- connectTimeout: 10s + http2ProtocolOptions: {} + loadAssignment: + clusterName: rate-limit-cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: ratelimit.svc.cluster.local + portValue: 8081 + name: rate-limit-cluster + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.endpoints.yaml new file mode 100644 index 00000000000..131cd47c730 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.endpoints.yaml @@ -0,0 +1,32 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 50 + locality: + region: first-route-dest/backend/0 + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 50 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 4.5.6.7 + portValue: 60000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.envoypatchpolicies.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.envoypatchpolicies.yaml new file mode 100644 index 00000000000..9508dd3e7b3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.envoypatchpolicies.yaml @@ -0,0 +1,16 @@ +- name: first-policy + namespace: default + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: foobar + namespace: default + conditions: + - lastTransitionTime: null + message: Patches have been successfully applied. + reason: Programmed + status: "True" + type: Programmed + controllerName: "" diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.listeners.yaml new file mode 100644 index 00000000000..08b5d410df5 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.listeners.yaml @@ -0,0 +1,64 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + drainType: MODIFY_ONLY + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: eg-ratelimit + failureModeDeny: true + rateLimitService: + grpcService: + envoyGrpc: + clusterName: rate-limit-cluster + transportApiVersion: V3 + timeout: 1s + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + preserveExternalRequestId: true + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: https-10080 + useRemoteAddress: true + name: first-listener + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + alpnProtocols: + - h2 + - http/1.1 + tlsCertificateSdsSecretConfigs: + - name: secret-1 + sdsConfig: + ads: {} + resourceApiVersion: V3 + - name: secret-2 + sdsConfig: + ads: {} + resourceApiVersion: V3 + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.routes.yaml new file mode 100644 index 00000000000..a7273c7a1b8 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.routes.yaml @@ -0,0 +1,36 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + rateLimits: + - actions: + - remoteAddress: {} + routes: + - match: + headers: + - name: user + stringMatch: + exact: jason + prefix: / + name: first-route + route: + cluster: first-route-dest + upgradeConfigs: + - upgradeType: websocket + - match: + headers: + - name: user + stringMatch: + exact: james + - name: country + stringMatch: + exact: US + prefix: / + name: second-route + route: + cluster: second-route-dest + upgradeConfigs: + - connectConfig: {} + upgradeType: CONNECT diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.secrets.yaml new file mode 100644 index 00000000000..d1c4b32fd5f --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-with-jsonpath.secrets.yaml @@ -0,0 +1,16 @@ +- name: secret-1 + tlsCertificate: + certificateChain: + inlineBytes: a2V5LWRhdGE= + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: secret-2 + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: test_secret + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 44d6d127bca..52f64d17ed5 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -55,6 +55,12 @@ func TestTranslateXds(t *testing.T) { "jsonpatch": { requireEnvoyPatchPolicies: true, }, + "jsonpatch-with-jsonpath": { + requireEnvoyPatchPolicies: true, + }, + "jsonpatch-add-op-empty-jsonpath": { + requireEnvoyPatchPolicies: true, + }, "jsonpatch-missing-resource": { requireEnvoyPatchPolicies: true, }, diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 6ff3e2ed4c7..f9f8fbc7c6b 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2117,7 +2117,8 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `op` | _[JSONPatchOperationType](#jsonpatchoperationtype)_ | true | Op is the type of operation to perform | -| `path` | _string_ | true | 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` | _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. | | `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/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 6ff3e2ed4c7..f9f8fbc7c6b 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -2117,7 +2117,8 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `op` | _[JSONPatchOperationType](#jsonpatchoperationtype)_ | true | Op is the type of operation to perform | -| `path` | _string_ | true | 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` | _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. | | `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. |