Skip to content

Commit

Permalink
catalog: add FailoverPolicy mutation and validation hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
rboyer committed Aug 4, 2023
1 parent 9d23acf commit 4b396e0
Show file tree
Hide file tree
Showing 4 changed files with 496 additions and 2 deletions.
7 changes: 7 additions & 0 deletions internal/catalog/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hashicorp/consul/internal/catalog/internal/types"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/resource"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
)

var (
Expand Down Expand Up @@ -94,3 +95,9 @@ func DefaultControllerDependencies() ControllerDependencies {
func RegisterControllers(mgr *controller.Manager, deps ControllerDependencies) {
controllers.Register(mgr, deps)
}

// SimplifyFailoverPolicy fully populates the PortConfigs map and clears the
// Configs map using the provided Service.
func SimplifyFailoverPolicy(svc *pbcatalog.Service, failover *pbcatalog.FailoverPolicy) *pbcatalog.FailoverPolicy {
return types.SimplifyFailoverPolicy(svc, failover)
}
211 changes: 209 additions & 2 deletions internal/catalog/internal/types/failover_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
package types

import (
"errors"
"fmt"

"github.com/hashicorp/go-multierror"
"google.golang.org/protobuf/proto"

"github.com/hashicorp/consul/internal/resource"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbresource"
Expand All @@ -27,7 +33,208 @@ func RegisterFailoverPolicy(r resource.Registry) {
r.Register(resource.Registration{
Type: FailoverPolicyV1Alpha1Type,
Proto: &pbcatalog.FailoverPolicy{},
Validate: nil,
Mutate: nil,
Mutate: MutateFailoverPolicy,
Validate: ValidateFailoverPolicy,
})
}

func MutateFailoverPolicy(res *pbresource.Resource) error {
var failover pbcatalog.FailoverPolicy

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

// Handle eliding empty configs.
if failover.Config != nil && failover.Config.IsEmpty() {
failover.Config = nil
}
for port, pc := range failover.PortConfigs {
if pc.IsEmpty() {
delete(failover.PortConfigs, port)
}
}
if len(failover.PortConfigs) == 0 {
failover.PortConfigs = nil
}

// TODO(rb): normalize dest ref tenancies

return res.Data.MarshalFrom(&failover)
}

func ValidateFailoverPolicy(res *pbresource.Resource) error {
var failover pbcatalog.FailoverPolicy

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

var merr error

if res.Id != nil && res.Id.Tenancy != nil {
if res.Id.Tenancy.PeerName != "local" {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "id",
Wrapped: resource.ErrInvalidField{
Name: "tenancy",
Wrapped: resource.ErrInvalidField{
Name: "peer_name",
Wrapped: fmt.Errorf("must be local"),
},
},
})
}
}

if failover.Config == nil && len(failover.PortConfigs) == 0 {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "config",
Wrapped: fmt.Errorf("at least one of config or port_configs must be set"),
})
}

if failover.Config != nil {
for _, err := range validateFailoverConfig(failover.Config, false) {
merr = multierror.Append(merr, resource.ErrInvalidField{
Name: "config",
Wrapped: err,
})
}
}

for port, pc := range failover.PortConfigs {
for _, err := range validateFailoverConfig(pc, true) {
merr = multierror.Append(merr, resource.ErrInvalidMapValue{
Map: "port_configs",
Key: port,
Wrapped: err,
})
}

// TODO: should sameness group be a ref once that's a resource?
}

return merr
}

func validateFailoverConfig(config *pbcatalog.FailoverConfig, ported bool) []error {
var errs []error

if (len(config.Destinations) > 0) == (config.SamenessGroup != "") {
errs = append(errs, resource.ErrInvalidField{
Name: "destinations",
Wrapped: fmt.Errorf("exactly one of destinations or sameness_group should be set"),
})
}
for i, dest := range config.Destinations {
for _, err := range validateFailoverPolicyDestination(dest, ported) {
errs = append(errs, resource.ErrInvalidListElement{
Name: "destinations",
Index: i,
Wrapped: err,
})
}
}

// TODO: validate sameness group requirements

return errs
}

func validateFailoverPolicyDestination(dest *pbcatalog.FailoverDestination, ported bool) []error {
var errs []error
if dest.Ref == nil {
errs = append(errs, resource.ErrInvalidField{
Name: "ref",
Wrapped: resource.ErrMissing,
})
} 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"),
},
})
}

// NOTE: Destinations here cannot define ports. Port equality is
// assumed and will be reconciled.
if !ported && dest.Port != "" {
errs = append(errs, resource.ErrInvalidField{
Name: "port",
Wrapped: fmt.Errorf("ports cannot be specified explicitly for the general failover section since it relies upon port alignment"),
})
}

hasPeer := dest.Ref.Tenancy.PeerName != "local"

if hasPeer && dest.Datacenter != "" {
errs = append(errs, resource.ErrInvalidField{
Name: "datacenter",
Wrapped: fmt.Errorf("ref.tenancy.peer_name and datacenter are mutually exclusive fields"),
})
}

return errs
}

// SimplifyFailoverPolicy fully populates the PortConfigs map and clears the
// Configs map using the provided Service.
func SimplifyFailoverPolicy(svc *pbcatalog.Service, failover *pbcatalog.FailoverPolicy) *pbcatalog.FailoverPolicy {
if failover == nil {
panic("failover is required")
}
if svc == nil {
panic("service is required")
}

// Copy so we can edit it.
dup := proto.Clone(failover)
failover = dup.(*pbcatalog.FailoverPolicy)

if failover.PortConfigs == nil {
failover.PortConfigs = make(map[string]*pbcatalog.FailoverConfig)
}

for _, port := range svc.Ports {
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
continue // skip
}

// TODO: this is wrong
if pc, ok := failover.PortConfigs[port.TargetPort]; ok {
for i, dest := range pc.Destinations {
// Assume port alignment.
if dest.Port == "" {
dest.Port = port.TargetPort
pc.Destinations[i] = dest
}
}
continue
}

if failover.Config != nil {
// Duplicate because each port will get this uniquely.
pc2 := proto.Clone(failover.Config).(*pbcatalog.FailoverConfig)
for _, dest := range pc2.Destinations {
dest.Port = port.TargetPort
}
failover.PortConfigs[port.TargetPort] = pc2
}
}

if failover.Config != nil {
failover.Config = nil
}

return failover
}
Loading

0 comments on commit 4b396e0

Please sign in to comment.