catalog: normalize/default/validate tenancy components of FailoverPolicy internal References (#18825)

FailoverPolicy resources contain inner Reference fields. We want to ensure 
that components of those reference Tenancy fields left unspecified are defaulted 
using the tenancy of the enclosing FailoverPolicy resource.

As the underlying helper being used to do the normalization calls the function 
modified in #18822, it also means that the PeerName field will be set to "local" for 
now automatically to avoid "local" != "" issues downstream.
This commit is contained in:
R.B. Boyer 2023-09-18 14:59:08 -05:00 committed by GitHub
parent 132c1eaa87
commit b4d5178e5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 283 additions and 71 deletions

View File

@ -126,3 +126,17 @@ type FailoverPolicyMapper interface {
func NewFailoverPolicyMapper() FailoverPolicyMapper { func NewFailoverPolicyMapper() FailoverPolicyMapper {
return failovermapper.New() return failovermapper.New()
} }
// ValidateLocalServiceRefNoSection ensures the following:
//
// - ref is non-nil
// - type is ServiceType
// - section is empty
// - tenancy is set and partition/namespace are both non-empty
// - peer_name must be "local"
//
// Each possible validation error is wrapped in the wrapErr function before
// being collected in a multierror.Error.
func ValidateLocalServiceRefNoSection(ref *pbresource.Reference, wrapErr func(error) error) error {
return types.ValidateLocalServiceRefNoSection(ref, wrapErr)
}

View File

@ -4,7 +4,6 @@
package types package types
import ( import (
"errors"
"fmt" "fmt"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
@ -53,10 +52,21 @@ func MutateFailoverPolicy(res *pbresource.Resource) error {
failover.Config = nil failover.Config = nil
changed = true changed = true
} }
if failover.Config != nil {
if mutateFailoverConfig(res.Id.Tenancy, failover.Config) {
changed = true
}
}
for port, pc := range failover.PortConfigs { for port, pc := range failover.PortConfigs {
if pc.IsEmpty() { if pc.IsEmpty() {
delete(failover.PortConfigs, port) delete(failover.PortConfigs, port)
changed = true changed = true
} else {
if mutateFailoverConfig(res.Id.Tenancy, pc) {
changed = true
}
} }
} }
if len(failover.PortConfigs) == 0 { if len(failover.PortConfigs) == 0 {
@ -64,8 +74,6 @@ func MutateFailoverPolicy(res *pbresource.Resource) error {
changed = true changed = true
} }
// TODO(rb): normalize dest ref tenancies
if !changed { if !changed {
return nil return nil
} }
@ -73,6 +81,42 @@ func MutateFailoverPolicy(res *pbresource.Resource) error {
return res.Data.MarshalFrom(&failover) return res.Data.MarshalFrom(&failover)
} }
func mutateFailoverConfig(policyTenancy *pbresource.Tenancy, config *pbcatalog.FailoverConfig) (changed bool) {
if policyTenancy != nil && !isLocalPeer(policyTenancy.PeerName) {
// TODO(peering/v2): remove this bypass when we know what to do with
// non-local peer references.
return false
}
for _, dest := range config.Destinations {
if dest.Ref == nil {
continue
}
if dest.Ref.Tenancy != nil && !isLocalPeer(dest.Ref.Tenancy.PeerName) {
// TODO(peering/v2): remove this bypass when we know what to do with
// non-local peer references.
continue
}
orig := proto.Clone(dest.Ref).(*pbresource.Reference)
resource.DefaultReferenceTenancy(
dest.Ref,
policyTenancy,
resource.DefaultNamespacedTenancy(), // Services are all namespace scoped.
)
if !proto.Equal(orig, dest.Ref) {
changed = true
}
}
return changed
}
func isLocalPeer(p string) bool {
return p == "local" || p == ""
}
func ValidateFailoverPolicy(res *pbresource.Resource) error { func ValidateFailoverPolicy(res *pbresource.Resource) error {
var failover pbcatalog.FailoverPolicy var failover pbcatalog.FailoverPolicy
@ -90,15 +134,25 @@ func ValidateFailoverPolicy(res *pbresource.Resource) error {
} }
if failover.Config != nil { if failover.Config != nil {
for _, err := range validateFailoverConfig(failover.Config, false) { wrapConfigErr := func(err error) error {
merr = multierror.Append(merr, resource.ErrInvalidField{ return resource.ErrInvalidField{
Name: "config", Name: "config",
Wrapped: err, Wrapped: err,
}) }
}
if cfgErr := validateFailoverConfig(failover.Config, false, wrapConfigErr); cfgErr != nil {
merr = multierror.Append(merr, cfgErr)
} }
} }
for portName, pc := range failover.PortConfigs { for portName, pc := range failover.PortConfigs {
wrapConfigErr := func(err error) error {
return resource.ErrInvalidMapValue{
Map: "port_configs",
Key: portName,
Wrapped: err,
}
}
if portNameErr := validatePortName(portName); portNameErr != nil { if portNameErr := validatePortName(portName); portNameErr != nil {
merr = multierror.Append(merr, resource.ErrInvalidMapKey{ merr = multierror.Append(merr, resource.ErrInvalidMapKey{
Map: "port_configs", Map: "port_configs",
@ -107,12 +161,8 @@ func ValidateFailoverPolicy(res *pbresource.Resource) error {
}) })
} }
for _, err := range validateFailoverConfig(pc, true) { if cfgErr := validateFailoverConfig(pc, true, wrapConfigErr); cfgErr != nil {
merr = multierror.Append(merr, resource.ErrInvalidMapValue{ merr = multierror.Append(merr, cfgErr)
Map: "port_configs",
Key: portName,
Wrapped: err,
})
} }
// TODO: should sameness group be a ref once that's a resource? // TODO: should sameness group be a ref once that's a resource?
@ -121,23 +171,29 @@ func ValidateFailoverPolicy(res *pbresource.Resource) error {
return merr return merr
} }
func validateFailoverConfig(config *pbcatalog.FailoverConfig, ported bool) []error { func validateFailoverConfig(config *pbcatalog.FailoverConfig, ported bool, wrapErr func(error) error) error {
var errs []error var merr error
if (len(config.Destinations) > 0) == (config.SamenessGroup != "") { if (len(config.Destinations) > 0) == (config.SamenessGroup != "") {
errs = append(errs, resource.ErrInvalidField{ merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "destinations", Name: "destinations",
Wrapped: fmt.Errorf("exactly one of destinations or sameness_group should be set"), // Wrapped: fmt.Errorf("exactly one of destinations or sameness_group should be set"),
}) Wrapped: fmt.Errorf("exactly one of destinations or sameness_group should be set: %v || %v",
(len(config.Destinations) > 0), (config.SamenessGroup != ""),
),
}))
} }
for i, dest := range config.Destinations { for i, dest := range config.Destinations {
for _, err := range validateFailoverPolicyDestination(dest, ported) { wrapDestErr := func(err error) error {
errs = append(errs, resource.ErrInvalidListElement{ return wrapErr(resource.ErrInvalidListElement{
Name: "destinations", Name: "destinations",
Index: i, Index: i,
Wrapped: err, Wrapped: err,
}) })
} }
if destErr := validateFailoverPolicyDestination(dest, ported, wrapDestErr); destErr != nil {
merr = multierror.Append(merr, destErr)
}
} }
switch config.Mode { switch config.Mode {
@ -146,72 +202,62 @@ func validateFailoverConfig(config *pbcatalog.FailoverConfig, ported bool) []err
case pbcatalog.FailoverMode_FAILOVER_MODE_SEQUENTIAL: case pbcatalog.FailoverMode_FAILOVER_MODE_SEQUENTIAL:
case pbcatalog.FailoverMode_FAILOVER_MODE_ORDER_BY_LOCALITY: case pbcatalog.FailoverMode_FAILOVER_MODE_ORDER_BY_LOCALITY:
default: default:
errs = append(errs, resource.ErrInvalidField{ merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "mode", Name: "mode",
Wrapped: fmt.Errorf("not a supported enum value: %v", config.Mode), Wrapped: fmt.Errorf("not a supported enum value: %v", config.Mode),
}) }))
} }
// TODO: validate sameness group requirements // TODO: validate sameness group requirements
return errs return merr
} }
func validateFailoverPolicyDestination(dest *pbcatalog.FailoverDestination, ported bool) []error { func validateFailoverPolicyDestination(dest *pbcatalog.FailoverDestination, ported bool, wrapErr func(error) error) error {
var errs []error var merr error
if dest.Ref == nil {
errs = append(errs, resource.ErrInvalidField{ wrapRefErr := func(err error) error {
return wrapErr(resource.ErrInvalidField{
Name: "ref", Name: "ref",
Wrapped: resource.ErrMissing, Wrapped: err,
})
} else if !resource.EqualType(dest.Ref.Type, ServiceType) {
errs = append(errs, resource.ErrInvalidField{
Name: "ref",
Wrapped: resource.ErrInvalidReferenceType{
AllowedType: ServiceType,
},
})
} else if dest.Ref.Section != "" {
errs = append(errs, resource.ErrInvalidField{
Name: "ref",
Wrapped: resource.ErrInvalidField{
Name: "section",
Wrapped: errors.New("section not supported for failover policy dest refs"),
},
}) })
} }
if refErr := ValidateLocalServiceRefNoSection(dest.Ref, wrapRefErr); refErr != nil {
merr = multierror.Append(merr, refErr)
}
// NOTE: Destinations here cannot define ports. Port equality is // NOTE: Destinations here cannot define ports. Port equality is
// assumed and will be reconciled. // assumed and will be reconciled.
if dest.Port != "" { if dest.Port != "" {
if ported { if ported {
if portNameErr := validatePortName(dest.Port); portNameErr != nil { if portNameErr := validatePortName(dest.Port); portNameErr != nil {
errs = append(errs, resource.ErrInvalidField{ merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "port", Name: "port",
Wrapped: portNameErr, Wrapped: portNameErr,
}) }))
} }
} else { } else {
errs = append(errs, resource.ErrInvalidField{ merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "port", Name: "port",
Wrapped: fmt.Errorf("ports cannot be specified explicitly for the general failover section since it relies upon port alignment"), Wrapped: fmt.Errorf("ports cannot be specified explicitly for the general failover section since it relies upon port alignment"),
}) }))
} }
} }
hasPeer := false hasPeer := false
if dest.Ref != nil { if dest.Ref != nil {
hasPeer = dest.Ref.Tenancy.PeerName != "local" hasPeer = dest.Ref.Tenancy.PeerName != "" && dest.Ref.Tenancy.PeerName != "local"
} }
if hasPeer && dest.Datacenter != "" { if hasPeer && dest.Datacenter != "" {
errs = append(errs, resource.ErrInvalidField{ merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "datacenter", Name: "datacenter",
Wrapped: fmt.Errorf("ref.tenancy.peer_name and datacenter are mutually exclusive fields"), Wrapped: fmt.Errorf("ref.tenancy.peer_name and datacenter are mutually exclusive fields"),
}) }))
} }
return errs return merr
} }
// SimplifyFailoverPolicy fully populates the PortConfigs map and clears the // SimplifyFailoverPolicy fully populates the PortConfigs map and clears the

View File

@ -4,6 +4,7 @@
package types package types
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -19,6 +20,7 @@ import (
func TestMutateFailoverPolicy(t *testing.T) { func TestMutateFailoverPolicy(t *testing.T) {
type testcase struct { type testcase struct {
policyTenancy *pbresource.Tenancy
failover *pbcatalog.FailoverPolicy failover *pbcatalog.FailoverPolicy
expect *pbcatalog.FailoverPolicy expect *pbcatalog.FailoverPolicy
expectErr string expectErr string
@ -26,6 +28,7 @@ func TestMutateFailoverPolicy(t *testing.T) {
run := func(t *testing.T, tc testcase) { run := func(t *testing.T, tc testcase) {
res := resourcetest.Resource(FailoverPolicyType, "api"). res := resourcetest.Resource(FailoverPolicyType, "api").
WithTenancy(tc.policyTenancy).
WithData(t, tc.failover). WithData(t, tc.failover).
Build() Build()
@ -134,6 +137,49 @@ func TestMutateFailoverPolicy(t *testing.T) {
}, },
}, },
}, },
"dest ref tenancy defaulting": {
policyTenancy: newTestTenancy("foo.bar"),
failover: &pbcatalog.FailoverPolicy{
Config: &pbcatalog.FailoverConfig{
Mode: pbcatalog.FailoverMode_FAILOVER_MODE_SEQUENTIAL,
Regions: []string{"foo", "bar"},
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRefWithTenancy(ServiceType, "", "api")},
{Ref: newRefWithTenancy(ServiceType, ".zim", "api")},
{Ref: newRefWithTenancy(ServiceType, "gir.zim", "api")},
},
},
PortConfigs: map[string]*pbcatalog.FailoverConfig{
"http": {
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRefWithTenancy(ServiceType, "", "api")},
{Ref: newRefWithTenancy(ServiceType, ".luthor", "api")},
{Ref: newRefWithTenancy(ServiceType, "lex.luthor", "api")},
},
},
},
},
expect: &pbcatalog.FailoverPolicy{
Config: &pbcatalog.FailoverConfig{
Mode: pbcatalog.FailoverMode_FAILOVER_MODE_SEQUENTIAL,
Regions: []string{"foo", "bar"},
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRefWithTenancy(ServiceType, "foo.bar", "api")},
{Ref: newRefWithTenancy(ServiceType, "foo.zim", "api")},
{Ref: newRefWithTenancy(ServiceType, "gir.zim", "api")},
},
},
PortConfigs: map[string]*pbcatalog.FailoverConfig{
"http": {
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRefWithTenancy(ServiceType, "foo.bar", "api")},
{Ref: newRefWithTenancy(ServiceType, "foo.luthor", "api")},
{Ref: newRefWithTenancy(ServiceType, "lex.luthor", "api")},
},
},
},
},
},
} }
for name, tc := range cases { for name, tc := range cases {
@ -224,7 +270,7 @@ func TestValidateFailoverPolicy(t *testing.T) {
{Ref: newRef(WorkloadType, "api-backup")}, {Ref: newRef(WorkloadType, "api-backup")},
}, },
}, },
expectErr: `invalid element at index 0 of list "destinations": invalid "ref" field: reference must have type catalog.v1alpha1.Service`, expectErr: `invalid element at index 0 of list "destinations": invalid "ref" field: invalid "type" field: reference must have type catalog.v1alpha1.Service`,
}, },
"dest: ref with section": { "dest: ref with section": {
config: &pbcatalog.FailoverConfig{ config: &pbcatalog.FailoverConfig{
@ -232,23 +278,25 @@ func TestValidateFailoverPolicy(t *testing.T) {
{Ref: resourcetest.Resource(ServiceType, "api").WithTenancy(resource.DefaultNamespacedTenancy()).Reference("blah")}, {Ref: resourcetest.Resource(ServiceType, "api").WithTenancy(resource.DefaultNamespacedTenancy()).Reference("blah")},
}, },
}, },
expectErr: `invalid element at index 0 of list "destinations": invalid "ref" field: invalid "section" field: section not supported for failover policy dest refs`, expectErr: `invalid element at index 0 of list "destinations": invalid "ref" field: invalid "section" field: section cannot be set here`,
},
"dest: ref peer and datacenter": {
config: &pbcatalog.FailoverConfig{
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRefWithPeer(ServiceType, "api", "peer1"), Datacenter: "dc2"},
},
},
expectErr: `invalid element at index 0 of list "destinations": invalid "datacenter" field: ref.tenancy.peer_name and datacenter are mutually exclusive fields`,
},
"dest: ref peer without datacenter": {
config: &pbcatalog.FailoverConfig{
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRefWithPeer(ServiceType, "api", "peer1")},
},
},
}, },
// TODO(v2/peering): re-enable when peering can exist
// "dest: ref peer and datacenter": {
// config: &pbcatalog.FailoverConfig{
// Destinations: []*pbcatalog.FailoverDestination{
// {Ref: newRefWithPeer(ServiceType, "api", "peer1"), Datacenter: "dc2"},
// },
// },
// expectErr: `invalid element at index 0 of list "destinations": invalid "datacenter" field: ref.tenancy.peer_name and datacenter are mutually exclusive fields`,
// },
// TODO(v2/peering): re-enable when peering can exist
// "dest: ref peer without datacenter": {
// config: &pbcatalog.FailoverConfig{
// Destinations: []*pbcatalog.FailoverDestination{
// {Ref: newRefWithPeer(ServiceType, "api", "peer1")},
// },
// },
// },
"dest: ref datacenter without peer": { "dest: ref datacenter without peer": {
config: &pbcatalog.FailoverConfig{ config: &pbcatalog.FailoverConfig{
Destinations: []*pbcatalog.FailoverDestination{ Destinations: []*pbcatalog.FailoverDestination{
@ -595,8 +643,33 @@ func newRef(typ *pbresource.Type, name string) *pbresource.Reference {
Reference("") Reference("")
} }
func newRefWithTenancy(typ *pbresource.Type, tenancyStr, name string) *pbresource.Reference {
return resourcetest.Resource(typ, name).
WithTenancy(newTestTenancy(tenancyStr)).
Reference("")
}
func newRefWithPeer(typ *pbresource.Type, name string, peer string) *pbresource.Reference { func newRefWithPeer(typ *pbresource.Type, name string, peer string) *pbresource.Reference {
ref := newRef(typ, name) ref := newRef(typ, name)
ref.Tenancy.PeerName = peer ref.Tenancy.PeerName = peer
return ref return ref
} }
func newTestTenancy(s string) *pbresource.Tenancy {
parts := strings.Split(s, ".")
switch len(parts) {
case 0:
return resource.DefaultClusteredTenancy()
case 1:
v := resource.DefaultPartitionedTenancy()
v.Partition = parts[0]
return v
case 2:
v := resource.DefaultNamespacedTenancy()
v.Partition = parts[0]
v.Namespace = parts[1]
return v
default:
return &pbresource.Tenancy{Partition: "BAD", Namespace: "BAD", PeerName: "BAD"}
}
}

View File

@ -4,6 +4,7 @@
package types package types
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"regexp" "regexp"
@ -237,3 +238,81 @@ func validateHealth(health pbcatalog.Health) error {
return resource.NewConstError(fmt.Sprintf("not a supported enum value: %v", health)) return resource.NewConstError(fmt.Sprintf("not a supported enum value: %v", health))
} }
} }
// ValidateLocalServiceRefNoSection ensures the following:
//
// - ref is non-nil
// - type is ServiceType
// - section is empty
// - tenancy is set and partition/namespace are both non-empty
// - peer_name must be "local"
//
// Each possible validation error is wrapped in the wrapErr function before
// being collected in a multierror.Error.
func ValidateLocalServiceRefNoSection(ref *pbresource.Reference, wrapErr func(error) error) error {
if ref == nil {
return wrapErr(resource.ErrMissing)
}
if !resource.EqualType(ref.Type, ServiceType) {
return wrapErr(resource.ErrInvalidField{
Name: "type",
Wrapped: resource.ErrInvalidReferenceType{
AllowedType: ServiceType,
},
})
}
var merr error
if ref.Section != "" {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "section",
Wrapped: errors.New("section cannot be set here"),
}))
}
if ref.Tenancy == nil {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "tenancy",
Wrapped: resource.ErrMissing,
}))
} else {
// NOTE: these are Service specific, since that's a Namespace-scoped type.
if ref.Tenancy.Partition == "" {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "tenancy",
Wrapped: resource.ErrInvalidField{
Name: "partition",
Wrapped: resource.ErrEmpty,
},
}))
}
if ref.Tenancy.Namespace == "" {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "tenancy",
Wrapped: resource.ErrInvalidField{
Name: "namespace",
Wrapped: resource.ErrEmpty,
},
}))
}
if ref.Tenancy.PeerName != "local" {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "tenancy",
Wrapped: resource.ErrInvalidField{
Name: "peer_name",
Wrapped: errors.New(`must be set to "local"`),
},
}))
}
}
if ref.Name == "" {
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
Name: "name",
Wrapped: resource.ErrMissing,
}))
}
return merr
}