mesh: add more validations to Destinations resource (#19202)

This commit is contained in:
Iryna Shustava 2023-10-13 16:52:20 -06:00 committed by GitHub
parent e94d6ceca6
commit 2ea33e9b86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 288 additions and 36 deletions

View File

@ -21,6 +21,7 @@ Data {
DestinationPort = "tcp"
IpPort = {
Ip = "127.0.0.1"
Port = 1234
}
}

View File

@ -110,3 +110,11 @@ func ValidateLocalServiceRefNoSection(ref *pbresource.Reference, wrapErr func(er
func ValidateSelector(sel *pbcatalog.WorkloadSelector, allowEmpty bool) error {
return types.ValidateSelector(sel, allowEmpty)
}
func ValidatePortName(name string) error {
return types.ValidatePortName(name)
}
func IsValidUnixSocketPath(host string) bool {
return types.IsValidUnixSocketPath(host)
}

View File

@ -145,7 +145,7 @@ func ValidateFailoverPolicy(res *pbresource.Resource) error {
Wrapped: err,
}
}
if portNameErr := validatePortName(portName); portNameErr != nil {
if portNameErr := ValidatePortName(portName); portNameErr != nil {
merr = multierror.Append(merr, resource.ErrInvalidMapKey{
Map: "port_configs",
Key: portName,
@ -245,7 +245,7 @@ func validateFailoverPolicyDestination(dest *pbcatalog.FailoverDestination, port
// assumed and will be reconciled.
if dest.Port != "" {
if ported {
if portNameErr := validatePortName(dest.Port); portNameErr != nil {
if portNameErr := ValidatePortName(dest.Port); portNameErr != nil {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "port",
Wrapped: portNameErr,

View File

@ -89,7 +89,7 @@ func ValidateService(res *pbresource.Resource) error {
}
// validate the target port
if nameErr := validatePortName(port.TargetPort); nameErr != nil {
if nameErr := ValidatePortName(port.TargetPort); nameErr != nil {
err = multierror.Append(err, resource.ErrInvalidListElement{
Name: "ports",
Index: idx,

View File

@ -126,7 +126,7 @@ func validateEndpoint(endpoint *pbcatalog.Endpoint, res *pbresource.Resource) er
// Validate the endpoints ports
for portName, port := range endpoint.Ports {
// Port names must be DNS labels
if portNameErr := validatePortName(portName); portNameErr != nil {
if portNameErr := ValidatePortName(portName); portNameErr != nil {
err = multierror.Append(err, resource.ErrInvalidMapKey{
Map: "ports",
Key: portName,

View File

@ -56,7 +56,7 @@ func isValidDNSLabel(label string) bool {
return dnsLabelMatcher.Match([]byte(label))
}
func isValidUnixSocketPath(host string) bool {
func IsValidUnixSocketPath(host string) bool {
if len(host) > maxUnixSocketPathLen || !strings.HasPrefix(host, "unix://") || strings.Contains(host, "\000") {
return false
}
@ -71,7 +71,7 @@ func validateWorkloadHost(host string) error {
}
// Check if the host represents an IP address, unix socket path or a DNS name
if !isValidIPAddress(host) && !isValidUnixSocketPath(host) && !isValidDNSName(host) {
if !isValidIPAddress(host) && !IsValidUnixSocketPath(host) && !isValidDNSName(host) {
return errInvalidWorkloadHostFormat{Host: host}
}
@ -139,7 +139,7 @@ func validateIPAddress(ip string) error {
return nil
}
func validatePortName(name string) error {
func ValidatePortName(name string) error {
if name == "" {
return resource.ErrEmpty
}
@ -184,7 +184,7 @@ func validateWorkloadAddress(addr *pbcatalog.WorkloadAddress, ports map[string]*
// Ensure that unix sockets reference exactly 1 port. They may also indirectly reference 1 port
// by the workload having only a single port and omitting any explicit port assignment.
if isValidUnixSocketPath(addr.Host) &&
if IsValidUnixSocketPath(addr.Host) &&
(len(addr.Ports) > 1 || (len(addr.Ports) == 0 && len(ports) > 1)) {
err = multierror.Append(err, errUnixSocketMultiport)
}

View File

@ -178,7 +178,7 @@ func TestIsValidUnixSocketPath(t *testing.T) {
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
require.Equal(t, tcase.valid, isValidUnixSocketPath(tcase.name))
require.Equal(t, tcase.valid, IsValidUnixSocketPath(tcase.name))
})
}
}
@ -361,15 +361,15 @@ func TestValidatePortName(t *testing.T) {
// test for the isValidDNSLabel function.
t.Run("empty", func(t *testing.T) {
require.Equal(t, resource.ErrEmpty, validatePortName(""))
require.Equal(t, resource.ErrEmpty, ValidatePortName(""))
})
t.Run("invalid", func(t *testing.T) {
require.Equal(t, errNotDNSLabel, validatePortName("foo.com"))
require.Equal(t, errNotDNSLabel, ValidatePortName("foo.com"))
})
t.Run("ok", func(t *testing.T) {
require.NoError(t, validatePortName("http"))
require.NoError(t, ValidatePortName("http"))
})
}

View File

@ -44,7 +44,7 @@ func ValidateWorkload(res *pbresource.Resource) error {
// Validate the Workload Ports
for portName, port := range workload.Ports {
if portNameErr := validatePortName(portName); portNameErr != nil {
if portNameErr := ValidatePortName(portName); portNameErr != nil {
err = multierror.Append(err, resource.ErrInvalidMapKey{
Map: "ports",
Key: portName,

View File

@ -553,7 +553,7 @@ func (suite *controllerTestSuite) TestController() {
}).Write(suite.T(), suite.client)
testutil.RunStep(suite.T(), "add explicit destinations and check that new proxy state is generated", func(t *testing.T) {
webProxyStateTemplate = suite.client.WaitForNewVersion(t, webProxyStateTemplateID, webProxyStateTemplate.Version)
webProxyStateTemplate = suite.client.WaitForNewVersion(suite.T(), webProxyStateTemplateID, webProxyStateTemplate.Version)
requireExplicitDestinationsFound(t, "api", webProxyStateTemplate)
})
@ -613,7 +613,7 @@ func (suite *controllerTestSuite) TestController() {
})
// We should get a new web proxy template resource because this destination should be removed.
webProxyStateTemplate = suite.client.WaitForNewVersion(t, webProxyStateTemplateID, webProxyStateTemplate.Version)
webProxyStateTemplate = suite.client.WaitForNewVersion(suite.T(), webProxyStateTemplateID, webProxyStateTemplate.Version)
requireExplicitDestinationsNotFound(t, "api", webProxyStateTemplate)
})

View File

@ -4,6 +4,8 @@
package types
import (
"net"
"github.com/hashicorp/go-multierror"
"google.golang.org/protobuf/proto"
@ -73,7 +75,6 @@ func ValidateDestinations(res *pbresource.Resource) error {
var merr error
// Validate the workload selector
if selErr := catalog.ValidateSelector(destinations.Workloads, false); selErr != nil {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "workloads",
@ -81,10 +82,17 @@ func ValidateDestinations(res *pbresource.Resource) error {
})
}
if destinations.GetPqDestinations() != nil {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "pq_destinations",
Wrapped: resource.ErrUnsupported,
})
}
for i, dest := range destinations.Destinations {
wrapDestErr := func(err error) error {
return resource.ErrInvalidListElement{
Name: "upstreams",
Name: "destinations",
Index: i,
Wrapped: err,
}
@ -101,8 +109,73 @@ func ValidateDestinations(res *pbresource.Resource) error {
merr = multierror.Append(merr, refErr)
}
// TODO(v2): validate port name using catalog validator
// TODO(v2): validate ListenAddr
if portErr := catalog.ValidatePortName(dest.DestinationPort); portErr != nil {
merr = multierror.Append(merr, wrapDestErr(resource.ErrInvalidField{
Name: "destination_port",
Wrapped: portErr,
}))
}
if dest.GetDatacenter() != "" {
merr = multierror.Append(merr, wrapDestErr(resource.ErrInvalidField{
Name: "datacenter",
Wrapped: resource.ErrUnsupported,
}))
}
if listenAddrErr := validateListenAddr(dest); listenAddrErr != nil {
merr = multierror.Append(merr, wrapDestErr(listenAddrErr))
}
}
return merr
}
func validateListenAddr(dest *pbmesh.Destination) error {
var merr error
if dest.GetListenAddr() == nil {
return multierror.Append(merr, resource.ErrInvalidFields{
Names: []string{"ip_port", "unix"},
Wrapped: resource.ErrMissingOneOf,
})
}
switch listenAddr := dest.GetListenAddr().(type) {
case *pbmesh.Destination_IpPort:
if ipPortErr := validateIPPort(listenAddr.IpPort); ipPortErr != nil {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "ip_port",
Wrapped: ipPortErr,
})
}
case *pbmesh.Destination_Unix:
if !catalog.IsValidUnixSocketPath(listenAddr.Unix.GetPath()) {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "unix",
Wrapped: resource.ErrInvalidField{
Name: "path",
Wrapped: errInvalidUnixSocketPath,
},
})
}
}
return merr
}
func validateIPPort(ipPort *pbmesh.IPPortAddress) error {
var merr error
if listenPortErr := validatePort(ipPort.GetPort(), "port"); listenPortErr != nil {
merr = multierror.Append(merr, listenPortErr)
}
if net.ParseIP(ipPort.GetIp()) == nil {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "ip",
Wrapped: errInvalidIP,
})
}
return merr

View File

@ -17,7 +17,7 @@ import (
"github.com/hashicorp/consul/sdk/testutil"
)
func TestMutateUpstreams(t *testing.T) {
func TestMutateDestinations(t *testing.T) {
type testcase struct {
tenancy *pbresource.Tenancy
data *pbmesh.Destinations
@ -86,7 +86,7 @@ func TestMutateUpstreams(t *testing.T) {
}
}
func TestValidateUpstreams(t *testing.T) {
func TestValidateDestinations(t *testing.T) {
type testcase struct {
data *pbmesh.Destinations
skipMutate bool
@ -151,7 +151,7 @@ func TestValidateUpstreams(t *testing.T) {
{DestinationRef: nil},
},
},
expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: missing required field`,
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: missing required field`,
},
"dest/bad type": {
skipMutate: true,
@ -163,7 +163,7 @@ func TestValidateUpstreams(t *testing.T) {
{DestinationRef: newRefWithTenancy(pbcatalog.WorkloadType, "default.default", "api")},
},
},
expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "type" field: reference must have type catalog.v2beta1.Service`,
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "type" field: reference must have type catalog.v2beta1.Service`,
},
"dest/nil tenancy": {
skipMutate: true,
@ -175,7 +175,7 @@ func TestValidateUpstreams(t *testing.T) {
{DestinationRef: &pbresource.Reference{Type: pbcatalog.ServiceType, Name: "api"}},
},
},
expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: missing required field`,
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: missing required field`,
},
"dest/bad dest tenancy/partition": {
skipMutate: true,
@ -187,7 +187,7 @@ func TestValidateUpstreams(t *testing.T) {
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, ".bar", "api")},
},
},
expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: invalid "partition" field: cannot be empty`,
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: invalid "partition" field: cannot be empty`,
},
"dest/bad dest tenancy/namespace": {
skipMutate: true,
@ -199,7 +199,7 @@ func TestValidateUpstreams(t *testing.T) {
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo", "api")},
},
},
expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: invalid "namespace" field: cannot be empty`,
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: invalid "namespace" field: cannot be empty`,
},
"dest/bad dest tenancy/peer_name": {
skipMutate: true,
@ -213,17 +213,158 @@ func TestValidateUpstreams(t *testing.T) {
Reference("")},
},
},
expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: invalid "peer_name" field: must be set to "local"`,
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: invalid "peer_name" field: must be set to "local"`,
},
"unsupported pq_destinations": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
PqDestinations: []*pbmesh.PreparedQueryDestination{
{Name: "foo-query"},
},
},
expectErr: `invalid "pq_destinations" field: field is currently not supported`,
},
"missing destination port": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 1234,
},
},
},
},
},
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_port" field: cannot be empty`,
},
"unsupported datacenter": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
Datacenter: "dc2",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 1234,
},
},
},
},
},
expectErr: `invalid element at index 0 of list "destinations": invalid "datacenter" field: field is currently not supported`,
},
"missing listen addr": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
},
},
},
expectErr: `invalid "ip_port,unix" fields: missing one of the required fields`,
},
"invalid ip for listen addr": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "invalid",
Port: 1234,
},
},
},
},
},
expectErr: `invalid "ip" field: IP address is not valid`,
},
"invalid port for listen addr": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 0,
},
},
},
},
},
expectErr: `invalid "port" field: port number is outside the range 1 to 65535`,
},
"invalid unix path for listen addr": {
skipMutate: true,
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
ListenAddr: &pbmesh.Destination_Unix{
Unix: &pbmesh.UnixSocketAddress{
Path: "foo",
},
},
},
},
},
expectErr: `invalid "unix" field: invalid "path" field: unix socket path is not valid`,
},
"normal": {
data: &pbmesh.Destinations{
Workloads: &pbcatalog.WorkloadSelector{
Names: []string{"blah"},
},
Workloads: &pbcatalog.WorkloadSelector{Names: []string{"foo"}},
Destinations: []*pbmesh.Destination{
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api")},
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.zim", "api")},
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "gir.zim", "api")},
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 1234,
},
},
},
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.zim", "api"),
DestinationPort: "p2",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 1235,
},
},
},
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "gir.zim", "api"),
DestinationPort: "p3",
ListenAddr: &pbmesh.Destination_Unix{
Unix: &pbmesh.UnixSocketAddress{
Path: "unix://foo/bar",
},
},
},
},
},
},
@ -234,9 +375,35 @@ func TestValidateUpstreams(t *testing.T) {
Filter: "metadata.foo == bar",
},
Destinations: []*pbmesh.Destination{
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api")},
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.zim", "api")},
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "gir.zim", "api")},
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"),
DestinationPort: "p1",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 1234,
},
},
},
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.zim", "api"),
DestinationPort: "p2",
ListenAddr: &pbmesh.Destination_IpPort{
IpPort: &pbmesh.IPPortAddress{
Ip: "127.0.0.1",
Port: 1235,
},
},
},
{
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "gir.zim", "api"),
DestinationPort: "p3",
ListenAddr: &pbmesh.Destination_Unix{
Unix: &pbmesh.UnixSocketAddress{
Path: "unix://foo/bar",
},
},
},
},
},
},

View File

@ -9,6 +9,8 @@ import (
var (
errInvalidPort = errors.New("port number is outside the range 1 to 65535")
errInvalidIP = errors.New("IP address is not valid")
errInvalidUnixSocketPath = errors.New("unix socket path is not valid")
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

@ -14,6 +14,7 @@ import (
var (
ErrMissing = NewConstError("missing required field")
ErrMissingOneOf = NewConstError("missing one of the required fields")
ErrEmpty = NewConstError("cannot be empty")
ErrReferenceTenancyNotEqual = NewConstError("resource tenancy and reference tenancy differ")
ErrUnsupported = NewConstError("field is currently not supported")