mesh: add validation hook to proxy configuration

pull/19209/head
Iryna Shustava 2023-10-12 18:11:00 -06:00 committed by R.B. Boyer
parent 3d1a606c3b
commit b08d9d4b47
7 changed files with 402 additions and 18 deletions

View File

@ -99,7 +99,8 @@ func TestSortProxyConfigurations(t *testing.T) {
var decProxyCfgs []*types.DecodedProxyConfiguration
for i, ws := range c.selectors {
proxyCfg := &pbmesh.ProxyConfiguration{
Workloads: ws,
Workloads: ws,
DynamicConfig: &pbmesh.DynamicConfig{},
}
resName := fmt.Sprintf("cfg-%d", i)
proxyCfgRes := resourcetest.Resource(pbmesh.ProxyConfigurationType, resName).

View File

@ -0,0 +1,11 @@
package types
import (
"errors"
)
var (
errInvalidPort = errors.New("port number is outside the range 1 to 65535")
errInvalidExposePathProtocol = errors.New("invalid protocol: only HTTP and HTTP2 protocols are allowed")
errMissingProxyConfigData = errors.New("at least one of \"bootstrap_config\" or \"dynamic_config\" fields must be set")
)

View File

@ -6,6 +6,11 @@ package types
import (
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/consul/internal/catalog"
"math"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/resource"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
@ -15,9 +20,9 @@ import (
func RegisterProxyConfiguration(r resource.Registry) {
r.Register(resource.Registration{
Type: pbmesh.ProxyConfigurationType,
Proto: &pbmesh.ProxyConfiguration{},
Scope: resource.ScopeNamespace,
Type: pbmesh.ProxyConfigurationType,
Proto: &pbmesh.ProxyConfiguration{},
Scope: resource.ScopeNamespace,
Mutate: MutateProxyConfiguration,
Validate: ValidateProxyConfiguration,
})
@ -53,23 +58,181 @@ func MutateProxyConfiguration(res *pbresource.Resource) error {
}
func ValidateProxyConfiguration(res *pbresource.Resource) error {
var cfg pbmesh.ProxyConfiguration
if err := res.Data.UnmarshalTo(&cfg); err != nil {
return resource.NewErrDataParse(&cfg, err)
decodedProxyCfg, decodeErr := resource.Decode[*pbmesh.ProxyConfiguration](res)
if decodeErr != nil {
return resource.NewErrDataParse(decodedProxyCfg.GetData(), decodeErr)
}
proxyCfg := decodedProxyCfg.GetData()
var merr error
var err error
// Validate the workload selector
if selErr := catalog.ValidateSelector(cfg.Workloads, false); selErr != nil {
merr = multierror.Append(merr, resource.ErrInvalidField{
if selErr := catalog.ValidateSelector(proxyCfg.Workloads, false); selErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "workloads",
Wrapped: selErr,
})
}
// TODO(rb): add more validation for proxy configuration
if proxyCfg.GetDynamicConfig() == nil && proxyCfg.GetBootstrapConfig() == nil {
err = multierror.Append(err, resource.ErrInvalidFields{
Names: []string{"dynamic_config", "bootstrap_config"},
Wrapped: errMissingProxyConfigData,
})
}
return merr
// nolint:staticcheck
if proxyCfg.GetOpaqueConfig() != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "opaque_config",
Wrapped: resource.ErrUnsupported,
})
}
if dynamicCfgErr := validateDynamicProxyConfiguration(proxyCfg.GetDynamicConfig()); dynamicCfgErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "dynamic_config",
Wrapped: dynamicCfgErr,
})
}
return err
}
func validateDynamicProxyConfiguration(cfg *pbmesh.DynamicConfig) error {
if cfg == nil {
return nil
}
var err error
// Error if any of the currently unsupported fields is set.
if cfg.GetMutualTlsMode() != pbmesh.MutualTLSMode_MUTUAL_TLS_MODE_DEFAULT {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "mutual_tls_mode",
Wrapped: resource.ErrUnsupported,
})
}
if cfg.GetMeshGatewayMode() != pbmesh.MeshGatewayMode_MESH_GATEWAY_MODE_UNSPECIFIED {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "mesh_gateway_mode",
Wrapped: resource.ErrUnsupported,
})
}
if cfg.GetAccessLogs() != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "access_logs",
Wrapped: resource.ErrUnsupported,
})
}
if cfg.GetEnvoyExtensions() != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "envoy_extensions",
Wrapped: resource.ErrUnsupported,
})
}
if cfg.GetPublicListenerJson() != "" {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "public_listener_json",
Wrapped: resource.ErrUnsupported,
})
}
if cfg.GetListenerTracingJson() != "" {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "listener_tracing_json",
Wrapped: resource.ErrUnsupported,
})
}
if cfg.GetLocalClusterJson() != "" {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "local_cluster_json",
Wrapped: resource.ErrUnsupported,
})
}
// nolint:staticcheck
if cfg.GetLocalWorkloadAddress() != "" {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "local_workload_address",
Wrapped: resource.ErrUnsupported,
})
}
// nolint:staticcheck
if cfg.GetLocalWorkloadPort() != 0 {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "local_workload_port",
Wrapped: resource.ErrUnsupported,
})
}
// nolint:staticcheck
if cfg.GetLocalWorkloadSocketPath() != "" {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "local_workload_socket_path",
Wrapped: resource.ErrUnsupported,
})
}
if tproxyCfg := cfg.GetTransparentProxy(); tproxyCfg != nil {
if tproxyCfg.DialedDirectly {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "transparent_proxy",
Wrapped: resource.ErrInvalidField{
Name: "dialed_directly",
Wrapped: resource.ErrUnsupported,
},
})
}
if outboundListenerPortErr := validatePort(tproxyCfg.OutboundListenerPort, "outbound_listener_port"); outboundListenerPortErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "transparent_proxy",
Wrapped: outboundListenerPortErr,
})
}
}
if exposeCfg := cfg.GetExposeConfig(); exposeCfg != nil {
for i, path := range exposeCfg.GetExposePaths() {
if listenerPortErr := validatePort(path.ListenerPort, "listener_port"); listenerPortErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "expose_config",
Wrapped: resource.ErrInvalidListElement{
Name: "expose_paths",
Index: i,
Wrapped: listenerPortErr,
},
})
}
if localPathPortErr := validatePort(path.LocalPathPort, "local_path_port"); localPathPortErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "expose_config",
Wrapped: resource.ErrInvalidListElement{
Name: "expose_paths",
Index: i,
Wrapped: localPathPortErr,
},
})
}
}
}
return err
}
func validatePort(port uint32, fieldName string) error {
if port < 1 || port > math.MaxUint16 {
return resource.ErrInvalidField{
Name: fieldName,
Wrapped: errInvalidPort,
}
}
return nil
}

View File

@ -4,9 +4,12 @@
package types
import (
"math"
"testing"
"github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/resourcetest"
@ -86,7 +89,196 @@ func TestMutateProxyConfiguration(t *testing.T) {
}
}
func TestValidateProxyConfiguration(t *testing.T) {
func TestValidateProxyConfiguration_MissingBothDynamicAndBootstrapConfig(t *testing.T) {
proxyCfg := &pbmesh.ProxyConfiguration{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
}
res := resourcetest.Resource(pbmesh.ProxyConfigurationType, "test").
WithData(t, proxyCfg).
Build()
err := ValidateProxyConfiguration(res)
var expError error
expError = multierror.Append(expError,
resource.ErrInvalidFields{
Names: []string{"dynamic_config", "bootstrap_config"},
Wrapped: errMissingProxyConfigData,
},
)
require.Equal(t, err, expError)
}
func TestValidateProxyConfiguration_AllFieldsInvalid(t *testing.T) {
proxyCfg := &pbmesh.ProxyConfiguration{
// Omit workload selector.
DynamicConfig: &pbmesh.DynamicConfig{
// Set unsupported fields.
MutualTlsMode: pbmesh.MutualTLSMode_MUTUAL_TLS_MODE_PERMISSIVE,
MeshGatewayMode: pbmesh.MeshGatewayMode_MESH_GATEWAY_MODE_LOCAL,
AccessLogs: &pbmesh.AccessLogsConfig{},
EnvoyExtensions: []*pbmesh.EnvoyExtension{{Name: "foo"}},
PublicListenerJson: "listener-json",
ListenerTracingJson: "tracing-json",
LocalClusterJson: "cluster-json",
LocalWorkloadAddress: "1.1.1.1",
LocalWorkloadPort: 1234,
LocalWorkloadSocketPath: "/foo/bar",
TransparentProxy: &pbmesh.TransparentProxy{
DialedDirectly: true, // unsupported
OutboundListenerPort: math.MaxUint16 + 1, // invalid
},
// Create invalid expose paths config.
ExposeConfig: &pbmesh.ExposeConfig{
ExposePaths: []*pbmesh.ExposePath{
{
ListenerPort: 0,
LocalPathPort: math.MaxUint16 + 1,
},
},
},
},
OpaqueConfig: &structpb.Struct{},
}
res := resourcetest.Resource(pbmesh.ProxyConfigurationType, "test").
WithData(t, proxyCfg).
Build()
err := ValidateProxyConfiguration(res)
var dynamicCfgErr error
unsupportedFields := []string{
"mutual_tls_mode",
"mesh_gateway_mode",
"access_logs",
"envoy_extensions",
"public_listener_json",
"listener_tracing_json",
"local_cluster_json",
"local_workload_address",
"local_workload_port",
"local_workload_socket_path",
}
for _, f := range unsupportedFields {
dynamicCfgErr = multierror.Append(dynamicCfgErr,
resource.ErrInvalidField{
Name: f,
Wrapped: resource.ErrUnsupported,
},
)
}
dynamicCfgErr = multierror.Append(dynamicCfgErr,
resource.ErrInvalidField{
Name: "transparent_proxy",
Wrapped: resource.ErrInvalidField{
Name: "dialed_directly",
Wrapped: resource.ErrUnsupported,
},
},
resource.ErrInvalidField{
Name: "transparent_proxy",
Wrapped: resource.ErrInvalidField{
Name: "outbound_listener_port",
Wrapped: errInvalidPort,
},
},
resource.ErrInvalidField{
Name: "expose_config",
Wrapped: resource.ErrInvalidListElement{
Name: "expose_paths",
Wrapped: resource.ErrInvalidField{
Name: "listener_port",
Wrapped: errInvalidPort,
},
},
},
resource.ErrInvalidField{
Name: "expose_config",
Wrapped: resource.ErrInvalidListElement{
Name: "expose_paths",
Wrapped: resource.ErrInvalidField{
Name: "local_path_port",
Wrapped: errInvalidPort,
},
},
},
)
var expError error
expError = multierror.Append(expError,
resource.ErrInvalidField{
Name: "workloads",
Wrapped: resource.ErrEmpty,
},
resource.ErrInvalidField{
Name: "opaque_config",
Wrapped: resource.ErrUnsupported,
},
resource.ErrInvalidField{
Name: "dynamic_config",
Wrapped: dynamicCfgErr,
},
)
require.Equal(t, err, expError)
}
func TestValidateProxyConfiguration_AllFieldsValid(t *testing.T) {
proxyCfg := &pbmesh.ProxyConfiguration{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
DynamicConfig: &pbmesh.DynamicConfig{
MutualTlsMode: pbmesh.MutualTLSMode_MUTUAL_TLS_MODE_DEFAULT,
MeshGatewayMode: pbmesh.MeshGatewayMode_MESH_GATEWAY_MODE_UNSPECIFIED,
TransparentProxy: &pbmesh.TransparentProxy{
DialedDirectly: false,
OutboundListenerPort: 15500,
},
ExposeConfig: &pbmesh.ExposeConfig{
ExposePaths: []*pbmesh.ExposePath{
{
ListenerPort: 1234,
LocalPathPort: 1235,
},
},
},
},
BootstrapConfig: &pbmesh.BootstrapConfig{
StatsdUrl: "stats-url",
DogstatsdUrl: "dogstats-url",
StatsTags: []string{"tags"},
PrometheusBindAddr: "prom-bind-addr",
StatsBindAddr: "stats-bind-addr",
ReadyBindAddr: "ready-bind-addr",
OverrideJsonTpl: "override-json-tpl",
StaticClustersJson: "static-clusters-json",
StaticListenersJson: "static-listeners-json",
StatsSinksJson: "stats-sinks-json",
StatsConfigJson: "stats-config-json",
StatsFlushInterval: "stats-flush-interval",
TracingConfigJson: "tracing-config-json",
TelemetryCollectorBindSocketDir: "telemetry-collector-bind-socket-dir",
},
}
res := resourcetest.Resource(pbmesh.ProxyConfigurationType, "test").
WithData(t, proxyCfg).
Build()
err := ValidateProxyConfiguration(res)
require.NoError(t, err)
}
func TestValidateProxyConfiguration_WorkloadSelector(t *testing.T) {
type testcase struct {
data *pbmesh.ProxyConfiguration
expectErr string

View File

@ -5,15 +5,18 @@ package resource
import (
"fmt"
"strings"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/hashicorp/consul/proto-public/pbresource"
"google.golang.org/protobuf/reflect/protoreflect"
)
var (
ErrMissing = NewConstError("missing required field")
ErrEmpty = NewConstError("cannot be empty")
ErrReferenceTenancyNotEqual = NewConstError("resource tenancy and reference tenancy differ")
ErrUnsupported = NewConstError("field is currently not supported")
)
// ConstError is more or less equivalent to the stdlib errors.errorstring. However, having
@ -147,3 +150,17 @@ type ErrInvalidReferenceType struct {
func (err ErrInvalidReferenceType) Error() string {
return fmt.Sprintf("reference must have type %s", ToGVK(err.AllowedType))
}
type ErrInvalidFields struct {
Names []string
Wrapped error
}
func (err ErrInvalidFields) Error() string {
allFields := strings.Join(err.Names, ",")
return fmt.Sprintf("invalid %q fields: %v", allFields, err.Wrapped)
}
func (err ErrInvalidFields) Unwrap() error {
return err.Wrapped
}

View File

@ -29,7 +29,7 @@ type MeshGatewayMode int32
const (
// MESH_GATEWAY_MODE_UNSPECIFIED represents no specific mode and should be
// used to indicate that a the decision on the mode will be made by other
// used to indicate that the decision on the mode will be made by other
// configuration or default settings.
MeshGatewayMode_MESH_GATEWAY_MODE_UNSPECIFIED MeshGatewayMode = 0
// MESH_GATEWAY_MODE_NONE is the mode to use when traffic should not be

View File

@ -9,7 +9,7 @@ package hashicorp.consul.mesh.v2beta1;
// +kubebuilder:validation:Type=string
enum MeshGatewayMode {
// MESH_GATEWAY_MODE_UNSPECIFIED represents no specific mode and should be
// used to indicate that a the decision on the mode will be made by other
// used to indicate that the decision on the mode will be made by other
// configuration or default settings.
MESH_GATEWAY_MODE_UNSPECIFIED = 0;