mirror of https://github.com/hashicorp/consul
mesh: add validation hook to proxy configuration
parent
3d1a606c3b
commit
b08d9d4b47
|
@ -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).
|
||||
|
|
|
@ -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")
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue