Skip to content

Commit

Permalink
Add type validations for the catalog resources
Browse files Browse the repository at this point in the history
Also adding some common resource validation error types to the internal/resource package.
  • Loading branch information
mkeeler committed May 10, 2023
1 parent 5030101 commit e2d93b9
Show file tree
Hide file tree
Showing 45 changed files with 3,192 additions and 7 deletions.
59 changes: 58 additions & 1 deletion internal/catalog/internal/types/dns_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
package types

import (
"math"

"github.com/hashicorp/consul/internal/resource"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/go-multierror"
)

const (
Expand All @@ -27,6 +30,60 @@ func RegisterDNSPolicy(r resource.Registry) {
r.Register(resource.Registration{
Type: DNSPolicyV1Alpha1Type,
Proto: &pbcatalog.DNSPolicy{},
Validate: nil,
Validate: ValidateDNSPolicy,
})
}

func ValidateDNSPolicy(res *pbresource.Resource) error {
var policy pbcatalog.DNSPolicy

if err := res.Data.UnmarshalTo(&policy); err != nil {
return resource.NewErrDataParse(&policy, err)
}

var err error
// Ensure that this resource isn't useless and is attempting to
// select at least one workload.
if selErr := validateSelector(policy.Workloads, false); selErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "workloads",
Wrapped: selErr,
})
}

// Validate the weights
if weightErr := validateDNSPolicyWeights(policy.Weights); weightErr != nil {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "weights",
Wrapped: weightErr,
})
}

return err
}

func validateDNSPolicyWeights(weights *pbcatalog.Weights) error {
// Non nil weights are required
if weights == nil {
return resource.ErrMissing
}

var err error
if weights.Passing < 1 || weights.Passing > math.MaxUint16 {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "passing",
Wrapped: errDNSPassingWeightOutOfRange,
})
}

// Each weight is an unsigned integer so we don't need to
// check for negative weights.
if weights.Warning > math.MaxUint16 {
err = multierror.Append(err, resource.ErrInvalidField{
Name: "warning",
Wrapped: errDNSWarningWeightOutOfRange,
})
}

return err
}
163 changes: 163 additions & 0 deletions internal/catalog/internal/types/dns_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package types

import (
"errors"
"fmt"
"testing"

"github.com/hashicorp/consul/internal/resource"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
)

func createDNSPolicyResource(t *testing.T, data protoreflect.ProtoMessage) *pbresource.Resource {
res := &pbresource.Resource{
Id: &pbresource.ID{
Type: DNSPolicyType,
Tenancy: &pbresource.Tenancy{
Partition: "default",
Namespace: "default",
PeerName: "local",
},
Name: "test-policy",
},
}

var err error
res.Data, err = anypb.New(data)
require.NoError(t, err)
return res
}

func TestValidateDNSPolicy_Ok(t *testing.T) {
data := &pbcatalog.DNSPolicy{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{""},
},
Weights: &pbcatalog.Weights{
Passing: 3,
Warning: 0,
},
}

res := createDNSPolicyResource(t, data)

err := ValidateDNSPolicy(res)
require.NoError(t, err)
}

func TestValidateDNSPolicy_ParseError(t *testing.T) {
// Any type other than the DNSPolicy type would work
// to cause the error we are expecting
data := &pbcatalog.IP{Address: "198.18.0.1"}

res := createDNSPolicyResource(t, data)

err := ValidateDNSPolicy(res)
require.Error(t, err)
require.True(t, errors.As(err, &resource.ErrDataParse{}))
}

func TestValidateDNSPolicy_MissingWeights(t *testing.T) {
data := &pbcatalog.DNSPolicy{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{""},
},
}

res := createDNSPolicyResource(t, data)

err := ValidateDNSPolicy(res)
require.Error(t, err)
expected := resource.ErrInvalidField{
Name: "weights",
Wrapped: resource.ErrMissing,
}
var actual resource.ErrInvalidField
require.True(t, errors.As(err, &actual))
require.Equal(t, expected, actual)
}

func TestValidateDNSPolicy_InvalidPassingWeight(t *testing.T) {
for _, weight := range []uint32{0, 1000000} {
t.Run(fmt.Sprintf("%d", weight), func(t *testing.T) {
data := &pbcatalog.DNSPolicy{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{""},
},
Weights: &pbcatalog.Weights{
Passing: weight,
},
}

res := createDNSPolicyResource(t, data)

err := ValidateDNSPolicy(res)
require.Error(t, err)
expected := resource.ErrInvalidField{
Name: "passing",
Wrapped: errDNSPassingWeightOutOfRange,
}
var actual resource.ErrInvalidField
require.True(t, errors.As(err, &actual))
require.Equal(t, "weights", actual.Name)
err = actual.Unwrap()
require.True(t, errors.As(err, &actual))
require.Equal(t, expected, actual)
})
}
}

func TestValidateDNSPolicy_InvalidWarningWeight(t *testing.T) {
data := &pbcatalog.DNSPolicy{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{""},
},
Weights: &pbcatalog.Weights{
Passing: 1,
Warning: 1000000,
},
}

res := createDNSPolicyResource(t, data)

err := ValidateDNSPolicy(res)
require.Error(t, err)
expected := resource.ErrInvalidField{
Name: "warning",
Wrapped: errDNSWarningWeightOutOfRange,
}
var actual resource.ErrInvalidField
require.True(t, errors.As(err, &actual))
require.Equal(t, "weights", actual.Name)
err = actual.Unwrap()
require.True(t, errors.As(err, &actual))
require.Equal(t, expected, actual)
}

func TestValidateDNSPolicy_EmptySelector(t *testing.T) {
data := &pbcatalog.DNSPolicy{
Weights: &pbcatalog.Weights{
Passing: 10,
Warning: 3,
},
}

res := createDNSPolicyResource(t, data)

err := ValidateDNSPolicy(res)
require.Error(t, err)
expected := resource.ErrInvalidField{
Name: "workloads",
Wrapped: resource.ErrEmpty,
}
var actual resource.ErrInvalidField
require.True(t, errors.As(err, &actual))
require.Equal(t, expected, actual)
}
62 changes: 62 additions & 0 deletions internal/catalog/internal/types/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package types

import (
"errors"
"fmt"
)

var (
errNotDNSLabel = errors.New(fmt.Sprintf("value must match regex: %s", dnsLabelRegex))
errNotIPAddress = errors.New("value is not a valid IP address")
errUnixSocketMultiport = errors.New("Unix socket address references more than one port")
errInvalidPhysicalPort = errors.New("port number is outside the range 1 to 65535")
errInvalidVirtualPort = errors.New("port number is outside the range 0 to 65535")
errDNSWarningWeightOutOfRange = errors.New("DNS warning weight is outside the range 0 to 65535")
errDNSPassingWeightOutOfRange = errors.New("DNS passing weight is outside of the range 1 to 65535")
errLocalityZoneNoRegion = errors.New("locality region cannot be empty if the zone is set")
errInvalidHealth = errors.New("health status must be one of: passing, warning, critical or maintenance")
)

type errInvalidWorkloadHostFormat struct {
Host string
}

func (err errInvalidWorkloadHostFormat) Error() string {
return fmt.Sprintf("%q is not an IP address, Unix socket path or a DNS name.", err.Host)
}

type errInvalidNodeHostFormat struct {
Host string
}

func (err errInvalidNodeHostFormat) Error() string {
return fmt.Sprintf("%q is not an IP address or a DNS name.", err.Host)
}

type errInvalidPortReference struct {
Name string
}

func (err errInvalidPortReference) Error() string {
return fmt.Sprintf("port with name %q has not been defined", err.Name)
}

type errVirtualPortReused struct {
Index int
Value uint32
}

func (err errVirtualPortReused) Error() string {
return fmt.Sprintf("virtual port %d was previously assigned at index %d", err.Value, err.Index)
}

type errTooMuchMesh struct {
Ports []string
}

func (err errTooMuchMesh) Error() string {
return fmt.Sprintf("protocol \"mesh\" was specified in more than 1 port: %+v", err.Ports)
}
70 changes: 70 additions & 0 deletions internal/catalog/internal/types/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package types

import (
"flag"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

// update allows golden files to be updated based on the current output.
var update = flag.Bool("update", false, "update golden files")

func goldenError(t *testing.T, name string, actual string) {
t.Helper()

fpath := filepath.Join("testdata", name+".golden")

if *update {
require.NoError(t, os.WriteFile(fpath, []byte(actual), 0644))
} else {
expected, err := os.ReadFile(fpath)
require.NoError(t, err)
require.Equal(t, string(expected), actual)
}
}

func TestErrorStrings(t *testing.T) {
type testCase struct {
err error
expected string
}

cases := map[string]error{
"errInvalidWorkloadHostFormat": errInvalidWorkloadHostFormat{
Host: "-foo-bar-",
},
"errInvalidNodeHostFormat": errInvalidNodeHostFormat{
Host: "unix:///node.sock",
},
"errInvalidPortReference": errInvalidPortReference{
Name: "http",
},
"errVirtualPortReused": errVirtualPortReused{
Index: 3,
Value: 8080,
},
"errTooMuchMesh": errTooMuchMesh{
Ports: []string{"http", "grpc"},
},
"errNotDNSLabel": errNotDNSLabel,
"errNotIPAddress": errNotIPAddress,
"errUnixSocketMultiport": errUnixSocketMultiport,
"errInvalidPhysicalPort": errInvalidPhysicalPort,
"errInvalidVirtualPort": errInvalidVirtualPort,
"errDNSWarningWeightOutOfRange": errDNSWarningWeightOutOfRange,
"errDNSPassingWeightOutOfRange": errDNSPassingWeightOutOfRange,
"errLocalityZoneNoRegion": errLocalityZoneNoRegion,
}

for name, err := range cases {
t.Run(name, func(t *testing.T) {
goldenError(t, name, err.Error())
})
}
}
Loading

0 comments on commit e2d93b9

Please sign in to comment.