Backport of Property Override validation improvements into release/1.16.x (#17778)
* backport of commit 97c779b5a2308a05fde93247209fa6e9cd3fc310 * backport of commit dd56a6800bebc54dabd7883fddc22b25ca2bdb92 --------- Co-authored-by: Michael Zalimeni <michael.zalimeni@hashicorp.com>
This commit is contained in:
parent
68e191dd50
commit
8e201f8964
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
extensions: Improve validation and error feedback for `property-override` builtin Envoy extension
|
||||||
|
```
|
|
@ -191,6 +191,10 @@ func (f *ResourceFilter) validate() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(f.Services) > 0 && f.TrafficDirection != extensioncommon.TrafficDirectionOutbound {
|
||||||
|
return fmt.Errorf("patch contains non-empty ResourceFilter.Services but ResourceFilter.TrafficDirection is not %q",
|
||||||
|
extensioncommon.TrafficDirectionOutbound)
|
||||||
|
}
|
||||||
for i := range f.Services {
|
for i := range f.Services {
|
||||||
sn := f.Services[i]
|
sn := f.Services[i]
|
||||||
sn.normalize()
|
sn.normalize()
|
||||||
|
|
|
@ -229,6 +229,20 @@ func TestConstructor(t *testing.T) {
|
||||||
ok: false,
|
ok: false,
|
||||||
errMsg: "service name is required",
|
errMsg: "service name is required",
|
||||||
},
|
},
|
||||||
|
"non-empty services with invalid traffic direction": {
|
||||||
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
||||||
|
makePatch(map[string]any{
|
||||||
|
"ResourceFilter": makeResourceFilter(map[string]any{
|
||||||
|
"TrafficDirection": extensioncommon.TrafficDirectionInbound,
|
||||||
|
"Services": []map[string]any{
|
||||||
|
{"Name:": "foo"},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}}),
|
||||||
|
ok: false,
|
||||||
|
errMsg: "patch contains non-empty ResourceFilter.Services but ResourceFilter.TrafficDirection is not \"outbound\"",
|
||||||
|
},
|
||||||
// See decode.HookWeakDecodeFromSlice for more details. In practice, we can end up
|
// See decode.HookWeakDecodeFromSlice for more details. In practice, we can end up
|
||||||
// with a "Patches" field decoded to the single "Patch" value contained in the
|
// with a "Patches" field decoded to the single "Patch" value contained in the
|
||||||
// serialized slice (raised from the containing slice). Using WeakDecode solves
|
// serialized slice (raised from the containing slice). Using WeakDecode solves
|
||||||
|
|
|
@ -75,7 +75,7 @@ func findTargetMessageAndField(m protoreflect.Message, parsedPath []string, patc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we have a non-terminal (parent) field in the path for which we
|
// Check whether we have a non-terminal (parent) field in the path for which we
|
||||||
// don't support child lookup.
|
// don't support child operations.
|
||||||
switch {
|
switch {
|
||||||
case fieldDesc.IsList():
|
case fieldDesc.IsList():
|
||||||
return nil, nil, fmt.Errorf("path contains member of repeated field '%s'; repeated field member access is not supported",
|
return nil, nil, fmt.Errorf("path contains member of repeated field '%s'; repeated field member access is not supported",
|
||||||
|
@ -83,6 +83,21 @@ func findTargetMessageAndField(m protoreflect.Message, parsedPath []string, patc
|
||||||
case fieldDesc.IsMap():
|
case fieldDesc.IsMap():
|
||||||
return nil, nil, fmt.Errorf("path contains member of map field '%s'; map field member access is not supported",
|
return nil, nil, fmt.Errorf("path contains member of map field '%s'; map field member access is not supported",
|
||||||
fieldName)
|
fieldName)
|
||||||
|
case fieldDesc.Message() != nil && fieldDesc.Message().FullName() == "google.protobuf.Any":
|
||||||
|
// Return a more helpful error for Any fields early.
|
||||||
|
//
|
||||||
|
// Doing this here prevents confusing two-step errors, e.g. "no match for field @type"
|
||||||
|
// on Any, when in fact we don't support variant proto message fields like Any in general.
|
||||||
|
// Because Any is a Message, we'd fail on invalid child fields or unsupported bytes target
|
||||||
|
// fields first.
|
||||||
|
//
|
||||||
|
// In the future, we could support Any by using the type field to initialize a struct for
|
||||||
|
// the nested message value.
|
||||||
|
return nil, nil, fmt.Errorf("variant-type message fields (google.protobuf.Any) are not supported")
|
||||||
|
case !(fieldDesc.Kind() == protoreflect.MessageKind):
|
||||||
|
// Non-Any fields that could be used to serialize protos as bytes will get a clear error message
|
||||||
|
// in this scenario. This also catches accidental use of non-complex fields as parent fields.
|
||||||
|
return nil, nil, fmt.Errorf("path contains member of non-message field '%s' (type '%s'); this type does not support child fields", fieldName, fieldDesc.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldM := m.Get(fieldDesc).Message()
|
fieldM := m.Get(fieldDesc).Message()
|
||||||
|
@ -137,6 +152,10 @@ func applyAdd(parentM protoreflect.Message, fieldDesc protoreflect.FieldDescript
|
||||||
// similar to a list (repeated field). This map handling is specific to _our_ patch semantics for
|
// similar to a list (repeated field). This map handling is specific to _our_ patch semantics for
|
||||||
// updating multiple message fields at once.
|
// updating multiple message fields at once.
|
||||||
if isMapValue && !fieldDesc.IsMap() {
|
if isMapValue && !fieldDesc.IsMap() {
|
||||||
|
if fieldDesc.Kind() != protoreflect.MessageKind {
|
||||||
|
return fmt.Errorf("non-message field type '%s' cannot be set via a map", fieldDesc.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
// Get a fresh copy of the target field's message, then set the children indicated by the patch.
|
// Get a fresh copy of the target field's message, then set the children indicated by the patch.
|
||||||
fieldM := parentM.Get(fieldDesc).Message().New()
|
fieldM := parentM.Get(fieldDesc).Message().New()
|
||||||
for k, v := range mapValue {
|
for k, v := range mapValue {
|
||||||
|
@ -151,6 +170,7 @@ func applyAdd(parentM protoreflect.Message, fieldDesc protoreflect.FieldDescript
|
||||||
fieldM.Set(targetFieldDesc, val)
|
fieldM.Set(targetFieldDesc, val)
|
||||||
}
|
}
|
||||||
parentM.Set(fieldDesc, protoreflect.ValueOf(fieldM))
|
parentM.Set(fieldDesc, protoreflect.ValueOf(fieldM))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Just set the field directly, as our patch value is not a map.
|
// Just set the field directly, as our patch value is not a map.
|
||||||
val, err := toProtoValue(parentM, fieldDesc, patch.Value)
|
val, err := toProtoValue(parentM, fieldDesc, patch.Value)
|
||||||
|
@ -280,6 +300,9 @@ func toProtoValue(parentM protoreflect.Message, fieldDesc protoreflect.FieldDesc
|
||||||
case float64:
|
case float64:
|
||||||
return toProtoNumericValue(fieldDesc, val)
|
return toProtoNumericValue(fieldDesc, val)
|
||||||
}
|
}
|
||||||
|
case protoreflect.BytesKind,
|
||||||
|
protoreflect.GroupKind:
|
||||||
|
return unsupportedTargetTypeErr(fieldDesc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to protoreflect.ValueOf, which may panic if an unexpected type is passed.
|
// Fall back to protoreflect.ValueOf, which may panic if an unexpected type is passed.
|
||||||
|
|
|
@ -2,6 +2,7 @@ package propertyoverride
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
|
@ -592,6 +593,31 @@ func TestPatchStruct(t *testing.T) {
|
||||||
},
|
},
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
|
"remove single field: Any": {
|
||||||
|
args: args{
|
||||||
|
k: &envoy_cluster_v3.Cluster{
|
||||||
|
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{
|
||||||
|
ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{
|
||||||
|
TypedConfig: &anypb.Any{
|
||||||
|
TypeUrl: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
patches: []Patch{
|
||||||
|
makeRemovePatch(
|
||||||
|
"/cluster_type/typed_config",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Invalid actual config, but used as an example of removing Any field directly
|
||||||
|
expected: &envoy_cluster_v3.Cluster{
|
||||||
|
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{
|
||||||
|
ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
"remove single field deeply nested": {
|
"remove single field deeply nested": {
|
||||||
args: args{
|
args: args{
|
||||||
k: &envoy_cluster_v3.Cluster{
|
k: &envoy_cluster_v3.Cluster{
|
||||||
|
@ -858,6 +884,69 @@ func TestPatchStruct(t *testing.T) {
|
||||||
ok: false,
|
ok: false,
|
||||||
errMsg: "unsupported target field type 'map'",
|
errMsg: "unsupported target field type 'map'",
|
||||||
},
|
},
|
||||||
|
"add unsupported target: non-message field via map": {
|
||||||
|
args: args{
|
||||||
|
k: &envoy_cluster_v3.Cluster{},
|
||||||
|
patches: []Patch{
|
||||||
|
makeAddPatch(
|
||||||
|
"/name",
|
||||||
|
map[string]any{
|
||||||
|
"cluster_refresh_rate": "5s",
|
||||||
|
"cluster_refresh_timeout": "3s",
|
||||||
|
"redirect_refresh_interval": "5s",
|
||||||
|
"redirect_refresh_threshold": 5,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ok: false,
|
||||||
|
errMsg: "non-message field type 'string' cannot be set via a map",
|
||||||
|
},
|
||||||
|
"add unsupported target: non-message parent field via single value": {
|
||||||
|
args: args{
|
||||||
|
k: &envoy_cluster_v3.Cluster{},
|
||||||
|
patches: []Patch{
|
||||||
|
makeAddPatch(
|
||||||
|
"/name/foo",
|
||||||
|
"bar",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ok: false,
|
||||||
|
errMsg: "path contains member of non-message field 'name' (type 'string'); this type does not support child fields",
|
||||||
|
},
|
||||||
|
"add unsupported target: non-message parent field via map": {
|
||||||
|
args: args{
|
||||||
|
k: &envoy_cluster_v3.Cluster{},
|
||||||
|
patches: []Patch{
|
||||||
|
makeAddPatch(
|
||||||
|
"/name/foo",
|
||||||
|
map[string]any{
|
||||||
|
"cluster_refresh_rate": "5s",
|
||||||
|
"cluster_refresh_timeout": "3s",
|
||||||
|
"redirect_refresh_interval": "5s",
|
||||||
|
"redirect_refresh_threshold": 5,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ok: false,
|
||||||
|
errMsg: "path contains member of non-message field 'name' (type 'string'); this type does not support child fields",
|
||||||
|
},
|
||||||
|
"add unsupported target: Any field": {
|
||||||
|
args: args{
|
||||||
|
k: &envoy_cluster_v3.Cluster{},
|
||||||
|
patches: []Patch{
|
||||||
|
makeAddPatch(
|
||||||
|
// Purposefully use a wrong-but-reasonable field name to ensure special error is returned
|
||||||
|
"/cluster_type/typed_config/@type",
|
||||||
|
"foo",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ok: false,
|
||||||
|
errMsg: "variant-type message fields (google.protobuf.Any) are not supported",
|
||||||
|
},
|
||||||
"add unsupported target: repeated message": {
|
"add unsupported target: repeated message": {
|
||||||
args: args{
|
args: args{
|
||||||
k: &envoy_cluster_v3.Cluster{},
|
k: &envoy_cluster_v3.Cluster{},
|
||||||
|
|
Loading…
Reference in New Issue