mirror of https://github.com/hashicorp/consul
[NET-5586][rebased] v2: Support virtual port references in config (#20371)
[OG Author: michael.zalimeni@hashicorp.com, rebase needed a separate PR] * v2: support virtual port in Service port references In addition to Service target port references, allow users to specify a port by stringified virtual port value. This is useful in environments such as Kubernetes where typical configuration is written in terms of Service virtual ports rather than workload (pod) target port names. Retaining the option of referencing target ports by name supports VMs, Nomad, and other use cases where virtual ports are not used by default. To support both uses cases at once, we will strictly interpret port references based on whether the value is numeric. See updated `ServicePort` docs for more details. * v2: update service ref docs for virtual port support Update proto and generated .go files with docs reflecting virtual port reference support. * v2: add virtual port references to L7 topo test Add coverage for mixed virtual and target port references to existing test. * update failover policy controller tests to work with computed failover policy and assert error conditions against FailoverPolicy and ComputedFailoverPolicy resources * accumulate services; don't overwrite them in enterprise --------- Co-authored-by: Michael Zalimeni <michael.zalimeni@hashicorp.com> Co-authored-by: R.B. Boyer <rb@hashicorp.com>pull/20389/head
parent
a2d50af1bd
commit
92aab7ea31
|
@ -35,7 +35,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ports": {
|
"ports": {
|
||||||
"external-service-port": {
|
"ext-svc-port": {
|
||||||
"port": 9876,
|
"port": 9876,
|
||||||
"protocol": "PROTOCOL_HTTP2"
|
"protocol": "PROTOCOL_HTTP2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"@type": "hashicorp.consul.catalog.v2beta1.Service",
|
"@type": "hashicorp.consul.catalog.v2beta1.Service",
|
||||||
"ports": [
|
"ports": [
|
||||||
{
|
{
|
||||||
"target_port": "external-service-port",
|
"target_port": "ext-svc-port",
|
||||||
"protocol": "PROTOCOL_HTTP2"
|
"protocol": "PROTOCOL_HTTP2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -149,7 +149,7 @@ func expectedFooServiceEndpoints() *pbcatalog.ServiceEndpoints {
|
||||||
{Host: "198.18.0.1"},
|
{Host: "198.18.0.1"},
|
||||||
},
|
},
|
||||||
Ports: map[string]*pbcatalog.WorkloadPort{
|
Ports: map[string]*pbcatalog.WorkloadPort{
|
||||||
"external-service-port": {
|
"ext-svc-port": {
|
||||||
Port: 9876,
|
Port: 9876,
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2,
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2,
|
||||||
},
|
},
|
||||||
|
|
|
@ -86,8 +86,8 @@ func ValidateSelector(sel *pbcatalog.WorkloadSelector, allowEmpty bool) error {
|
||||||
return types.ValidateSelector(sel, allowEmpty)
|
return types.ValidateSelector(sel, allowEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidatePortName(name string) error {
|
func ValidateServicePortID(id string) error {
|
||||||
return types.ValidatePortName(name)
|
return types.ValidateServicePortID(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsValidUnixSocketPath(host string) bool {
|
func IsValidUnixSocketPath(host string) bool {
|
||||||
|
|
|
@ -6,6 +6,9 @@ package failover
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/internal/catalog/internal/controllers/failover/expander"
|
"github.com/hashicorp/consul/internal/catalog/internal/controllers/failover/expander"
|
||||||
"github.com/hashicorp/consul/internal/catalog/internal/types"
|
"github.com/hashicorp/consul/internal/catalog/internal/types"
|
||||||
"github.com/hashicorp/consul/internal/controller"
|
"github.com/hashicorp/consul/internal/controller"
|
||||||
|
@ -16,8 +19,6 @@ import (
|
||||||
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
|
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
|
||||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -89,6 +90,8 @@ func (r *failoverPolicyReconciler) Reconcile(ctx context.Context, rt controller.
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Capture original raw config for pre-normalization status conditions.
|
||||||
|
rawFailoverPolicy := failoverPolicy.Data
|
||||||
|
|
||||||
// FailoverPolicy is name-aligned with the Service it controls.
|
// FailoverPolicy is name-aligned with the Service it controls.
|
||||||
serviceID := &pbresource.ID{
|
serviceID := &pbresource.ID{
|
||||||
|
@ -149,13 +152,13 @@ func (r *failoverPolicyReconciler) Reconcile(ctx context.Context, rt controller.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conds := computeNewConditions(failoverPolicy.Resource, newComputedFailoverPolicy, service, destServices, missingSamenessGroups)
|
conds := computeNewConditions(rawFailoverPolicy, failoverPolicy.Resource, newComputedFailoverPolicy, service, destServices, missingSamenessGroups)
|
||||||
if err := writeStatus(ctx, rt, failoverPolicy.Resource, conds); err != nil {
|
if err := writeStatus(ctx, rt, failoverPolicy.Resource, conds); err != nil {
|
||||||
rt.Logger.Error("error encountered when attempting to update the resource's failover policy status", "error", err)
|
rt.Logger.Error("error encountered when attempting to update the resource's failover policy status", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conds = computeNewConditions(computedFailoverResource, newComputedFailoverPolicy, service, destServices, missingSamenessGroups)
|
conds = computeNewConditions(rawFailoverPolicy, computedFailoverResource, newComputedFailoverPolicy, service, destServices, missingSamenessGroups)
|
||||||
if err := writeStatus(ctx, rt, computedFailoverResource, conds); err != nil {
|
if err := writeStatus(ctx, rt, computedFailoverResource, conds); err != nil {
|
||||||
rt.Logger.Error("error encountered when attempting to update the resource's computed failover policy status", "error", err)
|
rt.Logger.Error("error encountered when attempting to update the resource's computed failover policy status", "error", err)
|
||||||
return err
|
return err
|
||||||
|
@ -165,6 +168,7 @@ func (r *failoverPolicyReconciler) Reconcile(ctx context.Context, rt controller.
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeNewConditions(
|
func computeNewConditions(
|
||||||
|
rawFailoverPolicy *pbcatalog.FailoverPolicy,
|
||||||
fpRes *pbresource.Resource,
|
fpRes *pbresource.Resource,
|
||||||
fp *pbcatalog.ComputedFailoverPolicy,
|
fp *pbcatalog.ComputedFailoverPolicy,
|
||||||
service *resource.DecodedResource[*pbcatalog.Service],
|
service *resource.DecodedResource[*pbcatalog.Service],
|
||||||
|
@ -182,11 +186,27 @@ func computeNewConditions(
|
||||||
|
|
||||||
var conditions []*pbresource.Condition
|
var conditions []*pbresource.Condition
|
||||||
|
|
||||||
for port, pc := range fp.GetPortConfigs() {
|
if rawFailoverPolicy != nil {
|
||||||
if _, ok := allowedPortProtocols[port]; !ok {
|
// We need to validate port mappings on the raw input config due to the
|
||||||
conditions = append(conditions, ConditionUnknownPort(port))
|
// possibility of duplicate mappings, which will be normalized into one
|
||||||
}
|
// mapping by target port key.
|
||||||
|
usedTargetPorts := make(map[string]any)
|
||||||
|
for port := range rawFailoverPolicy.PortConfigs {
|
||||||
|
svcPort := service.Data.FindPortByID(port)
|
||||||
|
targetPort := svcPort.GetTargetPort() // svcPort could be nil
|
||||||
|
|
||||||
|
serviceRef := resource.NewReferenceKey(service.Id).ToReference()
|
||||||
|
if svcPort == nil {
|
||||||
|
conditions = append(conditions, ConditionUnknownPort(serviceRef, port))
|
||||||
|
} else if _, ok := usedTargetPorts[targetPort]; ok {
|
||||||
|
conditions = append(conditions, ConditionConflictDestinationPort(serviceRef, svcPort))
|
||||||
|
} else {
|
||||||
|
usedTargetPorts[targetPort] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pc := range fp.GetPortConfigs() {
|
||||||
for _, dest := range pc.Destinations {
|
for _, dest := range pc.Destinations {
|
||||||
// We know from validation that a Ref must be set, and the type it
|
// We know from validation that a Ref must be set, and the type it
|
||||||
// points to is a Service.
|
// points to is a Service.
|
||||||
|
|
|
@ -67,8 +67,9 @@ func TestController(t *testing.T) {
|
||||||
apiServiceData := &pbcatalog.Service{
|
apiServiceData := &pbcatalog.Service{
|
||||||
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
||||||
Ports: []*pbcatalog.ServicePort{{
|
Ports: []*pbcatalog.ServicePort{{
|
||||||
TargetPort: "http",
|
VirtualPort: 8080,
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
TargetPort: "http",
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
svc := rtest.Resource(pbcatalog.ServiceType, "api").
|
svc := rtest.Resource(pbcatalog.ServiceType, "api").
|
||||||
|
@ -108,7 +109,60 @@ func TestController(t *testing.T) {
|
||||||
|
|
||||||
t.Logf("reconciled to accepted")
|
t.Logf("reconciled to accepted")
|
||||||
|
|
||||||
// Update the failover to reference an unknown port
|
// Update the failover to reference a port twice (once by virtual, once by target port)
|
||||||
|
failoverData = &pbcatalog.FailoverPolicy{
|
||||||
|
PortConfigs: map[string]*pbcatalog.FailoverConfig{
|
||||||
|
"http": {
|
||||||
|
Destinations: []*pbcatalog.FailoverDestination{{
|
||||||
|
Ref: apiServiceRef,
|
||||||
|
Port: "http",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
"8080": {
|
||||||
|
Destinations: []*pbcatalog.FailoverDestination{{
|
||||||
|
Ref: apiServiceRef,
|
||||||
|
Port: "http",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
failover = rtest.Resource(pbcatalog.FailoverPolicyType, "api").
|
||||||
|
WithData(t, failoverData).
|
||||||
|
WithTenancy(tenancy).
|
||||||
|
Write(t, client)
|
||||||
|
|
||||||
|
t.Cleanup(func() { client.MustDelete(t, failover.Id) })
|
||||||
|
|
||||||
|
// Assert that the FailoverPolicy has the conflict condition.
|
||||||
|
client.WaitForStatusCondition(t, failover.Id, ControllerID, ConditionConflictDestinationPort(apiServiceRef, &pbcatalog.ServicePort{
|
||||||
|
VirtualPort: 8080,
|
||||||
|
TargetPort: "http",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Assert that the ComputedFailoverPolicy has the conflict condition.
|
||||||
|
// The port normalization that occurs in the call to SimplifyFailoverPolicy results in the port being
|
||||||
|
// removed from the final FailoverPolicy and ComputedFailoverPolicy.
|
||||||
|
expFailoverData := &pbcatalog.FailoverPolicy{
|
||||||
|
PortConfigs: map[string]*pbcatalog.FailoverConfig{
|
||||||
|
"http": {
|
||||||
|
Destinations: []*pbcatalog.FailoverDestination{{
|
||||||
|
Ref: apiServiceRef,
|
||||||
|
Port: "http",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedComputedFP = &pbcatalog.ComputedFailoverPolicy{
|
||||||
|
PortConfigs: expFailoverData.PortConfigs,
|
||||||
|
BoundReferences: []*pbresource.Reference{apiServiceRef},
|
||||||
|
}
|
||||||
|
waitAndAssertComputedFailoverPolicy(t, client, failover.Id, expectedComputedFP, ConditionConflictDestinationPort(apiServiceRef, &pbcatalog.ServicePort{
|
||||||
|
VirtualPort: 8080,
|
||||||
|
TargetPort: "http",
|
||||||
|
}))
|
||||||
|
t.Logf("reconciled to using duplicate destination port")
|
||||||
|
|
||||||
|
// Update the failover to fix the duplicate, but reference an unknown port
|
||||||
failoverData = &pbcatalog.FailoverPolicy{
|
failoverData = &pbcatalog.FailoverPolicy{
|
||||||
PortConfigs: map[string]*pbcatalog.FailoverConfig{
|
PortConfigs: map[string]*pbcatalog.FailoverConfig{
|
||||||
"http": {
|
"http": {
|
||||||
|
@ -132,11 +186,27 @@ func TestController(t *testing.T) {
|
||||||
|
|
||||||
t.Cleanup(func() { client.MustDelete(t, failover.Id) })
|
t.Cleanup(func() { client.MustDelete(t, failover.Id) })
|
||||||
|
|
||||||
|
// Assert that the FailoverPolicy has the unknown condition.
|
||||||
|
client.WaitForStatusCondition(t, failover.Id, ControllerID, ConditionUnknownPort(apiServiceRef, "admin"))
|
||||||
|
|
||||||
|
// Assert that the ComputedFailoverPolicy has the unknown condition.
|
||||||
|
// The port normalization that occurs in the call to SimplifyFailoverPolicy results in the port being
|
||||||
|
// removed from the final FailoverPolicy and ComputedFailoverPolicy.
|
||||||
|
expFailoverData = &pbcatalog.FailoverPolicy{
|
||||||
|
PortConfigs: map[string]*pbcatalog.FailoverConfig{
|
||||||
|
"http": {
|
||||||
|
Destinations: []*pbcatalog.FailoverDestination{{
|
||||||
|
Ref: apiServiceRef,
|
||||||
|
Port: "http",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
expectedComputedFP = &pbcatalog.ComputedFailoverPolicy{
|
expectedComputedFP = &pbcatalog.ComputedFailoverPolicy{
|
||||||
PortConfigs: failoverData.PortConfigs,
|
PortConfigs: expFailoverData.PortConfigs,
|
||||||
BoundReferences: []*pbresource.Reference{apiServiceRef},
|
BoundReferences: []*pbresource.Reference{apiServiceRef},
|
||||||
}
|
}
|
||||||
waitAndAssertComputedFailoverPolicy(t, client, failover.Id, expectedComputedFP, ConditionUnknownPort("admin"))
|
waitAndAssertComputedFailoverPolicy(t, client, failover.Id, expectedComputedFP, ConditionUnknownPort(apiServiceRef, "admin"))
|
||||||
t.Logf("reconciled to unknown admin port")
|
t.Logf("reconciled to unknown admin port")
|
||||||
|
|
||||||
// update the service to fix the stray reference, but point to a mesh port
|
// update the service to fix the stray reference, but point to a mesh port
|
||||||
|
@ -144,15 +214,22 @@ func TestController(t *testing.T) {
|
||||||
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
||||||
Ports: []*pbcatalog.ServicePort{
|
Ports: []*pbcatalog.ServicePort{
|
||||||
{
|
{
|
||||||
TargetPort: "http",
|
TargetPort: "http",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
VirtualPort: 8080,
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "admin",
|
TargetPort: "admin",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
|
VirtualPort: 10000,
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// update the expected ComputedFailoverPolicy to add back in the admin port as well
|
||||||
|
expectedComputedFP = &pbcatalog.ComputedFailoverPolicy{
|
||||||
|
PortConfigs: failoverData.PortConfigs,
|
||||||
|
BoundReferences: []*pbresource.Reference{apiServiceRef},
|
||||||
|
}
|
||||||
svc = rtest.Resource(pbcatalog.ServiceType, "api").
|
svc = rtest.Resource(pbcatalog.ServiceType, "api").
|
||||||
WithData(t, apiServiceData).
|
WithData(t, apiServiceData).
|
||||||
WithTenancy(tenancy).
|
WithTenancy(tenancy).
|
||||||
|
@ -168,12 +245,14 @@ func TestController(t *testing.T) {
|
||||||
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"api-"}},
|
||||||
Ports: []*pbcatalog.ServicePort{
|
Ports: []*pbcatalog.ServicePort{
|
||||||
{
|
{
|
||||||
TargetPort: "http",
|
VirtualPort: 8080,
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
TargetPort: "http",
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "admin",
|
VirtualPort: 10000,
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
TargetPort: "admin",
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -253,12 +332,14 @@ func TestController(t *testing.T) {
|
||||||
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"other-"}},
|
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"other-"}},
|
||||||
Ports: []*pbcatalog.ServicePort{
|
Ports: []*pbcatalog.ServicePort{
|
||||||
{
|
{
|
||||||
TargetPort: "http",
|
VirtualPort: 8080,
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
TargetPort: "http",
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "admin",
|
VirtualPort: 10000,
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
TargetPort: "admin",
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ package failover
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/consul/internal/resource"
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,6 +33,9 @@ const (
|
||||||
|
|
||||||
MissingSamenessGroupReason = "MissingSamenessGroup"
|
MissingSamenessGroupReason = "MissingSamenessGroup"
|
||||||
MissingSamenessGroupMessagePrefix = "referenced sameness group does not exist: "
|
MissingSamenessGroupMessagePrefix = "referenced sameness group does not exist: "
|
||||||
|
|
||||||
|
ConflictDestinationPortReason = "ConflictDestinationPort"
|
||||||
|
ConflictDestinationPortMessagePrefix = "multiple configs found for port on destination service: "
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -50,12 +54,12 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConditionUnknownPort(port string) *pbresource.Condition {
|
func ConditionUnknownPort(ref *pbresource.Reference, port string) *pbresource.Condition {
|
||||||
return &pbresource.Condition{
|
return &pbresource.Condition{
|
||||||
Type: StatusConditionAccepted,
|
Type: StatusConditionAccepted,
|
||||||
State: pbresource.Condition_STATE_FALSE,
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
Reason: UnknownPortReason,
|
Reason: UnknownPortReason,
|
||||||
Message: UnknownPortMessagePrefix + port,
|
Message: UnknownPortMessagePrefix + port + " on " + resource.ReferenceToString(ref),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,3 +98,12 @@ func ConditionMissingSamenessGroup(ref *pbresource.Reference) *pbresource.Condit
|
||||||
Message: MissingSamenessGroupMessagePrefix + resource.ReferenceToString(ref),
|
Message: MissingSamenessGroupMessagePrefix + resource.ReferenceToString(ref),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConditionConflictDestinationPort(ref *pbresource.Reference, port *pbcatalog.ServicePort) *pbresource.Condition {
|
||||||
|
return &pbresource.Condition{
|
||||||
|
Type: StatusConditionAccepted,
|
||||||
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
|
Reason: ConflictDestinationPortReason,
|
||||||
|
Message: ConflictDestinationPortMessagePrefix + port.ToPrintableString() + " on " + resource.ReferenceToString(ref),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,11 +9,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNotDNSLabel = errors.New(fmt.Sprintf("value must match regex: %s", dnsLabelRegex))
|
errNotDNSLabel = errors.New(fmt.Sprintf("value must be 1-63 characters and match regex: %s", dnsLabelRegex))
|
||||||
|
errNotPortName = errors.New(fmt.Sprintf("value must be 1-15 characters, contain at least 1 alpha, and match regex: %s", ianaSvcNameRegex))
|
||||||
errNotIPAddress = errors.New("value is not a valid IP address")
|
errNotIPAddress = errors.New("value is not a valid IP address")
|
||||||
errUnixSocketMultiport = errors.New("Unix socket address references more than one port")
|
errUnixSocketMultiport = errors.New("Unix socket address references more than one port")
|
||||||
errInvalidPhysicalPort = errors.New("port number is outside the range 1 to 65535")
|
errInvalidPhysicalPort = errors.New("port number is outside the range 1 to 65535")
|
||||||
errInvalidVirtualPort = errors.New("port number is outside the range 0 to 65535")
|
errInvalidVirtualPort = errors.New("port number is outside the range 0 to 65535")
|
||||||
|
errInvalidPortID = errors.New(fmt.Sprintf("value must be in the range 1 to 65535 or be 1-15 characters, contain at least 1 alpha, and match regex: %s", ianaSvcNameRegex))
|
||||||
errDNSWarningWeightOutOfRange = errors.New("DNS warning weight 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")
|
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")
|
errLocalityZoneNoRegion = errors.New("locality region cannot be empty if the zone is set")
|
||||||
|
|
|
@ -135,19 +135,19 @@ func validateCommonFailoverConfigs(res *pbcatalog.FailoverPolicy) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for portName, pc := range res.PortConfigs {
|
for portId, pc := range res.PortConfigs {
|
||||||
wrapConfigErr := func(err error) error {
|
wrapConfigErr := func(err error) error {
|
||||||
return resource.ErrInvalidMapValue{
|
return resource.ErrInvalidMapValue{
|
||||||
Map: "port_configs",
|
Map: "port_configs",
|
||||||
Key: portName,
|
Key: portId,
|
||||||
Wrapped: err,
|
Wrapped: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if portNameErr := ValidatePortName(portName); portNameErr != nil {
|
if portIdErr := ValidateServicePortID(portId); portIdErr != nil {
|
||||||
merr = multierror.Append(merr, resource.ErrInvalidMapKey{
|
merr = multierror.Append(merr, resource.ErrInvalidMapKey{
|
||||||
Map: "port_configs",
|
Map: "port_configs",
|
||||||
Key: portName,
|
Key: portId,
|
||||||
Wrapped: portNameErr,
|
Wrapped: portIdErr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,10 +234,10 @@ func validateFailoverPolicyDestination(dest *pbcatalog.FailoverDestination, port
|
||||||
// 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 portIdErr := ValidateServicePortID(dest.Port); portIdErr != nil {
|
||||||
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
|
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{
|
||||||
Name: "port",
|
Name: "port",
|
||||||
Wrapped: portNameErr,
|
Wrapped: portIdErr,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -281,6 +281,27 @@ func SimplifyFailoverPolicy(svc *pbcatalog.Service, failover *pbcatalog.Failover
|
||||||
failover.PortConfigs = make(map[string]*pbcatalog.FailoverConfig)
|
failover.PortConfigs = make(map[string]*pbcatalog.FailoverConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize all port configs to use the target port of the corresponding service port.
|
||||||
|
normalizedPortConfigs := make(map[string]*pbcatalog.FailoverConfig)
|
||||||
|
for port, pc := range failover.PortConfigs {
|
||||||
|
svcPort := svc.FindPortByID(port)
|
||||||
|
|
||||||
|
if svcPort != nil {
|
||||||
|
if _, ok := normalizedPortConfigs[svcPort.TargetPort]; ok {
|
||||||
|
// This is a duplicate virtual and target port mapping that will be reported as a status condition.
|
||||||
|
// Only update if this is the "canonical" mapping; otherwise, it's virtual, and we should ignore.
|
||||||
|
if port != svcPort.TargetPort {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizedPortConfigs[svcPort.TargetPort] = pc
|
||||||
|
}
|
||||||
|
// Else this is an invalid reference that will be reported as a status condition.
|
||||||
|
// Drop for safety and simpler output.
|
||||||
|
}
|
||||||
|
|
||||||
|
failover.PortConfigs = normalizedPortConfigs
|
||||||
|
|
||||||
for _, port := range svc.Ports {
|
for _, port := range svc.Ports {
|
||||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
continue // skip
|
continue // skip
|
||||||
|
|
|
@ -329,7 +329,7 @@ func getCommonFpCases() map[string]failoverTestcase {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: `invalid value of key "http" within port_configs: invalid element at index 0 of list "destinations": invalid "port" field: value must match regex: ^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`,
|
expectErr: `invalid value of key "http" within port_configs: invalid element at index 0 of list "destinations": invalid "port" field: value must be in the range 1 to 65535 or be 1-15 characters, contain at least 1 alpha, and match regex: ^[a-zA-Z0-9]+(?:-?[a-zA-Z0-9]+)*$`,
|
||||||
},
|
},
|
||||||
"ported config: bad ported in map": {
|
"ported config: bad ported in map": {
|
||||||
failover: &pbcatalog.FailoverPolicy{
|
failover: &pbcatalog.FailoverPolicy{
|
||||||
|
@ -341,7 +341,7 @@ func getCommonFpCases() map[string]failoverTestcase {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: `map port_configs contains an invalid key - "$bad$": value must match regex: ^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`,
|
expectErr: `map port_configs contains an invalid key - "$bad$": value must be in the range 1 to 65535 or be 1-15 characters, contain at least 1 alpha, and match regex: ^[a-zA-Z0-9]+(?:-?[a-zA-Z0-9]+)*$`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return fpcases
|
return fpcases
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/internal/catalog/workloadselector"
|
"github.com/hashicorp/consul/internal/catalog/workloadselector"
|
||||||
|
@ -105,7 +103,7 @@ func validateService(res *DecodedService) error {
|
||||||
// validate the virtual port is within the allowed range - 0 is allowed
|
// validate the virtual port is within the allowed range - 0 is allowed
|
||||||
// to signify that no virtual port should be used and the port will not
|
// to signify that no virtual port should be used and the port will not
|
||||||
// be available for transparent proxying within the mesh.
|
// be available for transparent proxying within the mesh.
|
||||||
if port.VirtualPort > math.MaxUint16 {
|
if portErr := ValidateVirtualPort(port.VirtualPort); portErr != nil {
|
||||||
err = multierror.Append(err, resource.ErrInvalidListElement{
|
err = multierror.Append(err, resource.ErrInvalidListElement{
|
||||||
Name: "ports",
|
Name: "ports",
|
||||||
Index: idx,
|
Index: idx,
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
|
@ -133,7 +131,6 @@ func validateEndpoint(endpoint *pbcatalog.Endpoint, res *pbresource.Resource) er
|
||||||
|
|
||||||
// Validate the endpoints ports
|
// Validate the endpoints ports
|
||||||
for portName, port := range endpoint.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{
|
err = multierror.Append(err, resource.ErrInvalidMapKey{
|
||||||
Map: "ports",
|
Map: "ports",
|
||||||
|
@ -155,13 +152,13 @@ func validateEndpoint(endpoint *pbcatalog.Endpoint, res *pbresource.Resource) er
|
||||||
|
|
||||||
// As the physical port is the real port the endpoint will be bound to
|
// As the physical port is the real port the endpoint will be bound to
|
||||||
// it must be in the standard 1-65535 range.
|
// it must be in the standard 1-65535 range.
|
||||||
if port.Port < 1 || port.Port > math.MaxUint16 {
|
if portErr := ValidatePhysicalPort(port.Port); portErr != nil {
|
||||||
err = multierror.Append(err, resource.ErrInvalidMapValue{
|
err = multierror.Append(err, resource.ErrInvalidMapValue{
|
||||||
Map: "ports",
|
Map: "ports",
|
||||||
Key: portName,
|
Key: portName,
|
||||||
Wrapped: resource.ErrInvalidField{
|
Wrapped: resource.ErrInvalidField{
|
||||||
Name: "physical_port",
|
Name: "physical_port",
|
||||||
Wrapped: errInvalidPhysicalPort,
|
Wrapped: portErr,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
value must match regex: ^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$
|
value must be 1-63 characters and match regex: ^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
@ -23,11 +24,21 @@ const (
|
||||||
// 108 characters is the max size that Linux (and probably other OSes) will
|
// 108 characters is the max size that Linux (and probably other OSes) will
|
||||||
// allow for the length of the Unix socket path.
|
// allow for the length of the Unix socket path.
|
||||||
maxUnixSocketPathLen = 108
|
maxUnixSocketPathLen = 108
|
||||||
|
|
||||||
|
// IANA service name. Applies to non-numeric port names in Consul and Kubernetes.
|
||||||
|
// See https://datatracker.ietf.org/doc/html/rfc6335#section-5.1 for definition.
|
||||||
|
// Length and at-least-one-alpha requirements are checked separately since
|
||||||
|
// Go re2 does not have lookaheads and for pattern legibility.
|
||||||
|
ianaSvcNameRegex = `^[a-zA-Z0-9]+(?:-?[a-zA-Z0-9]+)*$`
|
||||||
|
atLeastOneAlphaRegex = `^.*[a-zA-Z].*$`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dnsLabelRegex = `^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`
|
dnsLabelRegex = `^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`
|
||||||
dnsLabelMatcher = regexp.MustCompile(dnsLabelRegex)
|
dnsLabelMatcher = regexp.MustCompile(dnsLabelRegex)
|
||||||
|
|
||||||
|
ianaSvcNameMatcher = regexp.MustCompile(ianaSvcNameRegex)
|
||||||
|
atLeastOneAlphaMatcher = regexp.MustCompile(atLeastOneAlphaRegex)
|
||||||
)
|
)
|
||||||
|
|
||||||
func isValidIPAddress(host string) bool {
|
func isValidIPAddress(host string) bool {
|
||||||
|
@ -57,6 +68,19 @@ func isValidDNSLabel(label string) bool {
|
||||||
return dnsLabelMatcher.Match([]byte(label))
|
return dnsLabelMatcher.Match([]byte(label))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidPortName(name string) bool {
|
||||||
|
if len(name) > 15 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nameB := []byte(name)
|
||||||
|
return atLeastOneAlphaMatcher.Match(nameB) && ianaSvcNameMatcher.Match([]byte(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPhysicalPortNumber[V int | uint32](i V) bool {
|
||||||
|
return i > 0 && i <= math.MaxUint16
|
||||||
|
}
|
||||||
|
|
||||||
func IsValidUnixSocketPath(host string) bool {
|
func IsValidUnixSocketPath(host string) bool {
|
||||||
if len(host) > maxUnixSocketPathLen || !strings.HasPrefix(host, "unix://") || strings.Contains(host, "\000") {
|
if len(host) > maxUnixSocketPathLen || !strings.HasPrefix(host, "unix://") || strings.Contains(host, "\000") {
|
||||||
return false
|
return false
|
||||||
|
@ -145,8 +169,44 @@ func ValidatePortName(name string) error {
|
||||||
return resource.ErrEmpty
|
return resource.ErrEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isValidDNSLabel(name) {
|
if !isValidPortName(name) {
|
||||||
return errNotDNSLabel
|
return errNotPortName
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateServicePortID validates that the given string is a valid ID for referencing
|
||||||
|
// aservice port. This can be either a string virtual port number or target port name.
|
||||||
|
// See Service.ServicePort doc for more details.
|
||||||
|
func ValidateServicePortID(id string) error {
|
||||||
|
if id == "" {
|
||||||
|
return resource.ErrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidPortName(id) {
|
||||||
|
// Unlike an unset ServicePort.VirtualPort, a _reference_ to a service virtual
|
||||||
|
// port must be a real port number.
|
||||||
|
if i, err := strconv.Atoi(id); err != nil || !isValidPhysicalPortNumber(i) {
|
||||||
|
return errInvalidPortID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateVirtualPort[V int | uint32](port V) error {
|
||||||
|
// Allow 0 for unset virtual port values.
|
||||||
|
if port != 0 && !isValidPhysicalPortNumber(port) {
|
||||||
|
return errInvalidVirtualPort
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidatePhysicalPort[V int | uint32](port V) error {
|
||||||
|
if !isValidPhysicalPortNumber(port) {
|
||||||
|
return errInvalidPhysicalPort
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -48,6 +48,10 @@ func TestIsValidDNSLabel(t *testing.T) {
|
||||||
name: "1abc",
|
name: "1abc",
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
|
"fully-numeric": {
|
||||||
|
name: "1234",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
"underscore-start-not-allowed": {
|
"underscore-start-not-allowed": {
|
||||||
name: "_abc",
|
name: "_abc",
|
||||||
valid: false,
|
valid: false,
|
||||||
|
@ -151,6 +155,56 @@ func TestIsValidIPAddress(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIsValidPort tests both physical and virtual port validation using
|
||||||
|
// the same cases to ensure same coverage.
|
||||||
|
func TestIsValidPort(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
port int
|
||||||
|
validVirtual bool
|
||||||
|
validPhysical bool
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := map[string]testCase{
|
||||||
|
"negative": {
|
||||||
|
port: -1,
|
||||||
|
validPhysical: false,
|
||||||
|
validVirtual: false,
|
||||||
|
},
|
||||||
|
"zero": {
|
||||||
|
port: 0,
|
||||||
|
validPhysical: false,
|
||||||
|
validVirtual: true,
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
port: 1,
|
||||||
|
validPhysical: true,
|
||||||
|
validVirtual: true,
|
||||||
|
},
|
||||||
|
"8080": {
|
||||||
|
port: 8080,
|
||||||
|
validPhysical: true,
|
||||||
|
validVirtual: true,
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
port: 65535,
|
||||||
|
validPhysical: true,
|
||||||
|
validVirtual: true,
|
||||||
|
},
|
||||||
|
"above-max": {
|
||||||
|
port: 65536,
|
||||||
|
validPhysical: false,
|
||||||
|
validVirtual: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tcase := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
require.Equal(t, tcase.validPhysical, isValidPhysicalPortNumber(tcase.port))
|
||||||
|
require.Equal(t, tcase.validVirtual, ValidateVirtualPort(tcase.port) == nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsValidUnixSocketPath(t *testing.T) {
|
func TestIsValidUnixSocketPath(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
|
@ -354,22 +408,117 @@ func TestValidateIPAddress(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatePortName(t *testing.T) {
|
func TestValidatePortName(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := map[string]testCase{
|
||||||
|
"min-length": {
|
||||||
|
name: "a",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
"max-length": {
|
||||||
|
name: "a1b2c3d4e5f6g7h",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
"underscore-not-allowed": {
|
||||||
|
name: "has_underscores",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
"hyphenated": {
|
||||||
|
name: "has-hyphen3",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
"uppercase-allowed": {
|
||||||
|
name: "UPPERCASE",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
"numeric-start": {
|
||||||
|
name: "1abc",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
"numeric-start-with-hypen": {
|
||||||
|
name: "1-abc",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
"at-least-one-alpha-required": {
|
||||||
|
name: "1234",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
"hyphen-start-not-allowed": {
|
||||||
|
name: "-abc",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
"hyphen-end-not-allowed": {
|
||||||
|
name: "abc-",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
"unicode-not allowed": {
|
||||||
|
name: "abc∑",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
"too-long": {
|
||||||
|
name: strings.Repeat("a", 16),
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
"missing-name": {
|
||||||
|
name: "",
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tcase := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
err := ValidatePortName(tcase.name)
|
||||||
|
if tcase.valid {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
if tcase.name == "" {
|
||||||
|
require.Equal(t, resource.ErrEmpty, err)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, errNotPortName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePortID(t *testing.T) {
|
||||||
// this test does not perform extensive validation of what constitutes
|
// this test does not perform extensive validation of what constitutes
|
||||||
// a valid port name. In general the criteria is that it must not
|
// a valid port ID because it is a combination of ValidatePortName and
|
||||||
// be empty and must be a valid DNS label. Therefore extensive testing
|
// ValidateVirtualPort. In general the criteria is that it must not
|
||||||
// of what it means to be a valid DNS label is performed within the
|
// be empty and must be either a valid DNS label or stringified port
|
||||||
// test for the isValidDNSLabel function.
|
// number between 1 and 65535. Extensive testing is performed within the
|
||||||
|
// tests for those functions.
|
||||||
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
t.Run("empty", func(t *testing.T) {
|
||||||
require.Equal(t, resource.ErrEmpty, ValidatePortName(""))
|
require.Equal(t, resource.ErrEmpty, ValidateServicePortID(""))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid", func(t *testing.T) {
|
t.Run("invalid", func(t *testing.T) {
|
||||||
require.Equal(t, errNotDNSLabel, ValidatePortName("foo.com"))
|
require.Equal(t, errInvalidPortID, ValidateServicePortID("foo.com"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
require.Equal(t, errInvalidPortID, ValidateServicePortID("-1"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
require.Equal(t, errInvalidPortID, ValidateServicePortID("0"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
require.Equal(t, errInvalidPortID, ValidateServicePortID("65536"))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ok", func(t *testing.T) {
|
t.Run("ok", func(t *testing.T) {
|
||||||
require.NoError(t, ValidatePortName("http"))
|
require.NoError(t, ValidateServicePortID("http"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
require.NoError(t, ValidateServicePortID("8080"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,8 +213,8 @@ func validate(
|
||||||
return false, ConditionMeshProtocolNotFound(serviceRef)
|
return false, ConditionMeshProtocolNotFound(serviceRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.GetData().FindServicePort(dest.DestinationPort) != nil &&
|
if service.GetData().FindPortByID(dest.DestinationPort) != nil &&
|
||||||
service.GetData().FindServicePort(dest.DestinationPort).Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
service.GetData().FindPortByID(dest.DestinationPort).Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
return false, ConditionMeshProtocolDestinationPort(serviceRef, dest.DestinationPort)
|
return false, ConditionMeshProtocolDestinationPort(serviceRef, dest.DestinationPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,7 @@ func (r *routesReconciler) Reconcile(ctx context.Context, rt controller.Runtime,
|
||||||
pending := make(PendingStatuses)
|
pending := make(PendingStatuses)
|
||||||
|
|
||||||
ValidateXRouteReferences(related, pending)
|
ValidateXRouteReferences(related, pending)
|
||||||
|
ValidateDestinationPolicyPorts(related, pending)
|
||||||
|
|
||||||
generatedResults := GenerateComputedRoutes(related, pending)
|
generatedResults := GenerateComputedRoutes(related, pending)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/consul/internal/mesh/internal/controllers/routes/loader"
|
||||||
|
"github.com/hashicorp/consul/internal/mesh/internal/types"
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateDestinationPolicyPorts examines the ported configs of the policies provided
|
||||||
|
// and issues status conditions if anything is unacceptable.
|
||||||
|
func ValidateDestinationPolicyPorts(related *loader.RelatedResources, pending PendingStatuses) {
|
||||||
|
for rk, destPolicy := range related.DestinationPolicies {
|
||||||
|
conditions := computeNewDestPolicyPortConditions(related, rk, destPolicy)
|
||||||
|
pending.AddConditions(rk, destPolicy.Resource, conditions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeNewDestPolicyPortConditions(
|
||||||
|
related serviceGetter,
|
||||||
|
rk resource.ReferenceKey,
|
||||||
|
destPolicy *types.DecodedDestinationPolicy,
|
||||||
|
) []*pbresource.Condition {
|
||||||
|
var conditions []*pbresource.Condition
|
||||||
|
|
||||||
|
// Since this is name-aligned, just switch the type to fetch the service.
|
||||||
|
service := related.GetService(resource.ReplaceType(pbcatalog.ServiceType, rk.ToID()))
|
||||||
|
if service != nil {
|
||||||
|
allowedPortProtocols := make(map[string]pbcatalog.Protocol)
|
||||||
|
for _, port := range service.Data.Ports {
|
||||||
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
|
continue // skip
|
||||||
|
}
|
||||||
|
allowedPortProtocols[port.TargetPort] = port.Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
usedTargetPorts := make(map[string]any)
|
||||||
|
for port := range destPolicy.Data.PortConfigs {
|
||||||
|
svcPort := service.Data.FindPortByID(port)
|
||||||
|
targetPort := svcPort.GetTargetPort() // svcPort could be nil
|
||||||
|
|
||||||
|
serviceRef := resource.NewReferenceKey(service.Id)
|
||||||
|
if svcPort == nil {
|
||||||
|
conditions = append(conditions, ConditionUnknownDestinationPort(serviceRef.ToReference(), port))
|
||||||
|
} else if _, ok := usedTargetPorts[targetPort]; ok {
|
||||||
|
conditions = append(conditions, ConditionConflictDestinationPort(serviceRef.ToReference(), svcPort))
|
||||||
|
} else {
|
||||||
|
usedTargetPorts[targetPort] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conditions = append(conditions, ConditionDestinationServiceNotFound(rk.ToReference()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/catalog"
|
||||||
|
"github.com/hashicorp/consul/internal/mesh/internal/types"
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
|
||||||
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
|
"github.com/hashicorp/consul/proto/private/prototest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComputeNewDestPolicyPortConditions(t *testing.T) {
|
||||||
|
registry := resource.NewRegistry()
|
||||||
|
types.Register(registry)
|
||||||
|
catalog.RegisterTypes(registry)
|
||||||
|
|
||||||
|
type protocolAndVirtualPort struct {
|
||||||
|
protocol pbcatalog.Protocol
|
||||||
|
virtualPort uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
newService := func(name string, ports map[string]protocolAndVirtualPort) *types.DecodedService {
|
||||||
|
var portSlice []*pbcatalog.ServicePort
|
||||||
|
for targetPort, pv := range ports {
|
||||||
|
portSlice = append(portSlice, &pbcatalog.ServicePort{
|
||||||
|
TargetPort: targetPort,
|
||||||
|
VirtualPort: pv.virtualPort,
|
||||||
|
Protocol: pv.protocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
svc := rtest.Resource(pbcatalog.ServiceType, name).
|
||||||
|
WithData(t, &pbcatalog.Service{Ports: portSlice}).
|
||||||
|
Build()
|
||||||
|
rtest.ValidateAndNormalize(t, registry, svc)
|
||||||
|
|
||||||
|
dec, err := resource.Decode[*pbcatalog.Service](svc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return dec
|
||||||
|
}
|
||||||
|
|
||||||
|
newDestPolicy := func(name string, portConfigs map[string]*pbmesh.DestinationConfig) *types.DecodedDestinationPolicy {
|
||||||
|
policy := rtest.Resource(pbmesh.DestinationPolicyType, name).
|
||||||
|
WithData(t, &pbmesh.DestinationPolicy{PortConfigs: portConfigs}).
|
||||||
|
Build()
|
||||||
|
rtest.ValidateAndNormalize(t, registry, policy)
|
||||||
|
|
||||||
|
dec, err := resource.Decode[*pbmesh.DestinationPolicy](policy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return dec
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("with no service", func(t *testing.T) {
|
||||||
|
sg := newTestServiceGetter()
|
||||||
|
got := computeNewDestPolicyPortConditions(sg, resource.NewReferenceKey(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy())),
|
||||||
|
newDestPolicy("dest", map[string]*pbmesh.DestinationConfig{
|
||||||
|
"http": defaultDestConfig(),
|
||||||
|
}))
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
prototest.AssertContainsElement(t, got, ConditionDestinationServiceNotFound(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with service and using missing port", func(t *testing.T) {
|
||||||
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
|
}))
|
||||||
|
got := computeNewDestPolicyPortConditions(sg, resource.NewReferenceKey(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy())),
|
||||||
|
newDestPolicy("dest", map[string]*pbmesh.DestinationConfig{
|
||||||
|
"grpc": defaultDestConfig(),
|
||||||
|
}))
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
prototest.AssertContainsElement(t, got, ConditionUnknownDestinationPort(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()),
|
||||||
|
"grpc",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with service and using duplicate port", func(t *testing.T) {
|
||||||
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
|
}))
|
||||||
|
got := computeNewDestPolicyPortConditions(sg, resource.NewReferenceKey(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy())),
|
||||||
|
newDestPolicy("dest", map[string]*pbmesh.DestinationConfig{
|
||||||
|
"http": defaultDestConfig(),
|
||||||
|
"8080": defaultDestConfig(),
|
||||||
|
}))
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
prototest.AssertContainsElement(t, got, ConditionConflictDestinationPort(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()),
|
||||||
|
&pbcatalog.ServicePort{VirtualPort: 8080, TargetPort: "http"},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with service and using correct port", func(t *testing.T) {
|
||||||
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
|
}))
|
||||||
|
got := computeNewDestPolicyPortConditions(sg, resource.NewReferenceKey(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy())),
|
||||||
|
newDestPolicy("dest", map[string]*pbmesh.DestinationConfig{
|
||||||
|
"http": defaultDestConfig(),
|
||||||
|
}))
|
||||||
|
require.Empty(t, got)
|
||||||
|
})
|
||||||
|
}
|
|
@ -134,8 +134,13 @@ func compile(
|
||||||
wildcardedPort = true
|
wildcardedPort = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if _, ok := allowedPortProtocols[ref.Port]; ok {
|
// Check for valid port reference and implicitly convert virtual
|
||||||
ports = append(ports, ref.Port)
|
// port references to target port. From this point on, all port
|
||||||
|
// matching should be against a workload target port. The same
|
||||||
|
// normalization must be done for destination services below.
|
||||||
|
svcPort := parentServiceDec.Data.FindPortByID(ref.Port)
|
||||||
|
if _, ok := allowedPortProtocols[svcPort.GetTargetPort()]; ok {
|
||||||
|
ports = append(ports, svcPort.TargetPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -330,9 +335,17 @@ func compile(
|
||||||
// failover legs into here.
|
// failover legs into here.
|
||||||
for _, details := range mc.Targets {
|
for _, details := range mc.Targets {
|
||||||
svcRef := details.BackendRef.Ref
|
svcRef := details.BackendRef.Ref
|
||||||
|
|
||||||
|
svc := related.GetService(svcRef)
|
||||||
|
if svc == nil {
|
||||||
|
panic("impossible at this point; should already have been handled before getting here")
|
||||||
|
}
|
||||||
|
// Already added to bound refs above, so skip needless check here
|
||||||
|
|
||||||
destPolicy := related.GetDestinationPolicyForService(svcRef)
|
destPolicy := related.GetDestinationPolicyForService(svcRef)
|
||||||
if destPolicy != nil {
|
if destPolicy != nil {
|
||||||
portDestConfig, ok := destPolicy.Data.PortConfigs[details.BackendRef.Port]
|
simpleDestPolicy := types.SimplifyDestinationPolicy(svc.Data, destPolicy.Data)
|
||||||
|
portDestConfig, ok := simpleDestPolicy.PortConfigs[details.BackendRef.Port]
|
||||||
if ok {
|
if ok {
|
||||||
boundRefCollector.AddRefOrID(destPolicy.Resource.Id)
|
boundRefCollector.AddRefOrID(destPolicy.Resource.Id)
|
||||||
details.DestinationConfig = portDestConfig
|
details.DestinationConfig = portDestConfig
|
||||||
|
@ -458,10 +471,12 @@ func compileFailoverConfig(
|
||||||
}
|
}
|
||||||
|
|
||||||
var backendTargetName string
|
var backendTargetName string
|
||||||
ok, meshPort := shouldRouteTrafficToBackend(svc, backendRef)
|
rPorts, ok := shouldRouteTrafficToBackend(svc, backendRef)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue // skip this leg of failover for now
|
continue // skip this leg of failover for now
|
||||||
}
|
}
|
||||||
|
// Map virtual port to target port if used.
|
||||||
|
backendRef.Port = rPorts.targetPort
|
||||||
|
|
||||||
destTargetName := types.BackendRefToComputedRoutesTarget(backendRef)
|
destTargetName := types.BackendRefToComputedRoutesTarget(backendRef)
|
||||||
|
|
||||||
|
@ -470,7 +485,7 @@ func compileFailoverConfig(
|
||||||
targets[destTargetName] = &pbmesh.BackendTargetDetails{
|
targets[destTargetName] = &pbmesh.BackendTargetDetails{
|
||||||
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_INDIRECT,
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_INDIRECT,
|
||||||
BackendRef: backendRef,
|
BackendRef: backendRef,
|
||||||
MeshPort: meshPort,
|
MeshPort: rPorts.meshPort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
backendTargetName = destTargetName
|
backendTargetName = destTargetName
|
||||||
|
@ -555,11 +570,13 @@ func compileHTTPRouteNode(
|
||||||
if backendSvc != nil {
|
if backendSvc != nil {
|
||||||
brc.AddRefOrID(backendSvc.Resource.Id)
|
brc.AddRefOrID(backendSvc.Resource.Id)
|
||||||
}
|
}
|
||||||
if ok, meshPort := shouldRouteTrafficToBackend(backendSvc, backendRef.BackendRef); ok {
|
if rPorts, ok := shouldRouteTrafficToBackend(backendSvc, backendRef.BackendRef); ok {
|
||||||
|
// Map virtual port to target port if used.
|
||||||
|
backendRef.BackendRef.Port = rPorts.targetPort
|
||||||
details := &pbmesh.BackendTargetDetails{
|
details := &pbmesh.BackendTargetDetails{
|
||||||
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
||||||
BackendRef: backendRef.BackendRef,
|
BackendRef: backendRef.BackendRef,
|
||||||
MeshPort: meshPort,
|
MeshPort: rPorts.meshPort,
|
||||||
}
|
}
|
||||||
backendTarget = node.AddTarget(backendRef.BackendRef, details)
|
backendTarget = node.AddTarget(backendRef.BackendRef, details)
|
||||||
} else {
|
} else {
|
||||||
|
@ -622,11 +639,13 @@ func compileGRPCRouteNode(
|
||||||
if backendSvc != nil {
|
if backendSvc != nil {
|
||||||
brc.AddRefOrID(backendSvc.Resource.Id)
|
brc.AddRefOrID(backendSvc.Resource.Id)
|
||||||
}
|
}
|
||||||
if ok, meshPort := shouldRouteTrafficToBackend(backendSvc, backendRef.BackendRef); ok {
|
if rPorts, ok := shouldRouteTrafficToBackend(backendSvc, backendRef.BackendRef); ok {
|
||||||
|
// Map virtual port to target port if used.
|
||||||
|
backendRef.BackendRef.Port = rPorts.targetPort
|
||||||
details := &pbmesh.BackendTargetDetails{
|
details := &pbmesh.BackendTargetDetails{
|
||||||
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
||||||
BackendRef: backendRef.BackendRef,
|
BackendRef: backendRef.BackendRef,
|
||||||
MeshPort: meshPort,
|
MeshPort: rPorts.meshPort,
|
||||||
}
|
}
|
||||||
backendTarget = node.AddTarget(backendRef.BackendRef, details)
|
backendTarget = node.AddTarget(backendRef.BackendRef, details)
|
||||||
} else {
|
} else {
|
||||||
|
@ -681,11 +700,13 @@ func compileTCPRouteNode(
|
||||||
if backendSvc != nil {
|
if backendSvc != nil {
|
||||||
brc.AddRefOrID(backendSvc.Resource.Id)
|
brc.AddRefOrID(backendSvc.Resource.Id)
|
||||||
}
|
}
|
||||||
if ok, meshPort := shouldRouteTrafficToBackend(backendSvc, backendRef.BackendRef); ok {
|
if rPorts, ok := shouldRouteTrafficToBackend(backendSvc, backendRef.BackendRef); ok {
|
||||||
|
// Map virtual port to target port if used.
|
||||||
|
backendRef.BackendRef.Port = rPorts.targetPort
|
||||||
details := &pbmesh.BackendTargetDetails{
|
details := &pbmesh.BackendTargetDetails{
|
||||||
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
||||||
BackendRef: backendRef.BackendRef,
|
BackendRef: backendRef.BackendRef,
|
||||||
MeshPort: meshPort,
|
MeshPort: rPorts.meshPort,
|
||||||
}
|
}
|
||||||
backendTarget = node.AddTarget(backendRef.BackendRef, details)
|
backendTarget = node.AddTarget(backendRef.BackendRef, details)
|
||||||
} else {
|
} else {
|
||||||
|
@ -705,15 +726,20 @@ func compileTCPRouteNode(
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldRouteTrafficToBackend(backendSvc *types.DecodedService, backendRef *pbmesh.BackendReference) (bool, string) {
|
type routableBackendPorts struct {
|
||||||
|
meshPort, targetPort string
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRouteTrafficToBackend(backendSvc *types.DecodedService, backendRef *pbmesh.BackendReference) (*routableBackendPorts, bool) {
|
||||||
if backendSvc == nil {
|
if backendSvc == nil {
|
||||||
return false, ""
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
found = false
|
found = false
|
||||||
inMesh = false
|
inMesh = false
|
||||||
meshPort string
|
meshPort string
|
||||||
|
targetPort string
|
||||||
)
|
)
|
||||||
for _, port := range backendSvc.Data.Ports {
|
for _, port := range backendSvc.Data.Ports {
|
||||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
|
@ -721,12 +747,14 @@ func shouldRouteTrafficToBackend(backendSvc *types.DecodedService, backendRef *p
|
||||||
meshPort = port.TargetPort
|
meshPort = port.TargetPort
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if port.TargetPort == backendRef.Port {
|
if port.MatchesPortId(backendRef.Port) {
|
||||||
found = true
|
found = true
|
||||||
|
// Map virtual port to target port if used.
|
||||||
|
targetPort = port.TargetPort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return inMesh && found, meshPort
|
return &routableBackendPorts{meshPort, targetPort}, inMesh && found
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDefaultRouteNode(
|
func createDefaultRouteNode(
|
||||||
|
|
|
@ -1682,5 +1682,274 @@ func TestGenerateComputedRoutes(t *testing.T) {
|
||||||
}}
|
}}
|
||||||
run(t, related, expect, nil)
|
run(t, related, expect, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Same as above dest case, but tests various combinations of virtual and target port values
|
||||||
|
t.Run("http route with dest policy - virtual ports", func(t *testing.T) {
|
||||||
|
for _, parentRefPort := range []string{"http", "8080"} {
|
||||||
|
for _, backendRefPort := range []string{"http", "9090", ""} {
|
||||||
|
for _, configKeyPort := range []string{"http", "9090"} {
|
||||||
|
t.Run(fmt.Sprintf("%v, %v, %v", parentRefPort, backendRefPort, configKeyPort), func(t *testing.T) {
|
||||||
|
apiServiceData := &pbcatalog.Service{
|
||||||
|
Workloads: &pbcatalog.WorkloadSelector{
|
||||||
|
Prefixes: []string{"api-"},
|
||||||
|
},
|
||||||
|
Ports: []*pbcatalog.ServicePort{
|
||||||
|
{TargetPort: "mesh", VirtualPort: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
||||||
|
{TargetPort: "http", VirtualPort: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fooServiceData := &pbcatalog.Service{
|
||||||
|
Workloads: &pbcatalog.WorkloadSelector{
|
||||||
|
Prefixes: []string{"foo-"},
|
||||||
|
},
|
||||||
|
Ports: []*pbcatalog.ServicePort{
|
||||||
|
{TargetPort: "mesh", VirtualPort: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
||||||
|
{TargetPort: "http", VirtualPort: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRoute1 := &pbmesh.HTTPRoute{
|
||||||
|
ParentRefs: []*pbmesh.ParentReference{
|
||||||
|
newParentRef(newRef(pbcatalog.ServiceType, "api", tenancy), parentRefPort),
|
||||||
|
},
|
||||||
|
Rules: []*pbmesh.HTTPRouteRule{{
|
||||||
|
Matches: []*pbmesh.HTTPRouteMatch{{
|
||||||
|
Path: &pbmesh.HTTPPathMatch{
|
||||||
|
Type: pbmesh.PathMatchType_PATH_MATCH_TYPE_PREFIX,
|
||||||
|
Value: "/",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
BackendRefs: []*pbmesh.HTTPBackendRef{{
|
||||||
|
BackendRef: newBackendRef(fooServiceRef, backendRefPort, ""),
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
destPolicy := &pbmesh.DestinationPolicy{
|
||||||
|
PortConfigs: map[string]*pbmesh.DestinationConfig{
|
||||||
|
configKeyPort: {
|
||||||
|
ConnectTimeout: durationpb.New(55 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedPortDestConfig := &pbmesh.DestinationConfig{
|
||||||
|
ConnectTimeout: durationpb.New(55 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
related := loader.NewRelatedResources().
|
||||||
|
AddComputedRoutesIDs(apiComputedRoutesID).
|
||||||
|
AddResources(
|
||||||
|
newService("api", apiServiceData),
|
||||||
|
newService("foo", fooServiceData),
|
||||||
|
newHTTPRoute("api-http-route1", httpRoute1),
|
||||||
|
newDestPolicy("foo", destPolicy),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Same result as non-virtual-port variant of test
|
||||||
|
expect := []*ComputedRoutesResult{{
|
||||||
|
ID: apiComputedRoutesID,
|
||||||
|
OwnerID: apiServiceID,
|
||||||
|
Data: &pbmesh.ComputedRoutes{
|
||||||
|
BoundReferences: []*pbresource.Reference{
|
||||||
|
apiServiceRef,
|
||||||
|
fooServiceRef,
|
||||||
|
newRef(pbmesh.DestinationPolicyType, "foo", tenancy),
|
||||||
|
newRef(pbmesh.HTTPRouteType, "api-http-route1", tenancy),
|
||||||
|
},
|
||||||
|
PortedConfigs: map[string]*pbmesh.ComputedPortRoutes{
|
||||||
|
"http": {
|
||||||
|
Config: &pbmesh.ComputedPortRoutes_Http{
|
||||||
|
Http: &pbmesh.ComputedHTTPRoute{
|
||||||
|
Rules: []*pbmesh.ComputedHTTPRouteRule{
|
||||||
|
{
|
||||||
|
Matches: defaultHTTPRouteMatches(),
|
||||||
|
BackendRefs: []*pbmesh.ComputedHTTPBackendRef{{
|
||||||
|
BackendTarget: backendName("foo", "http"),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Matches: defaultHTTPRouteMatches(),
|
||||||
|
BackendRefs: []*pbmesh.ComputedHTTPBackendRef{{
|
||||||
|
BackendTarget: types.NullRouteBackend,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ParentRef: newParentRef(apiServiceRef, "http"),
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
|
Targets: map[string]*pbmesh.BackendTargetDetails{
|
||||||
|
backendName("foo", "http"): {
|
||||||
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
||||||
|
MeshPort: "mesh",
|
||||||
|
BackendRef: newBackendRef(fooServiceRef, "http", ""),
|
||||||
|
DestinationConfig: expectedPortDestConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
run(t, related, expect, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Same as above failover case, but tests various combinations of virtual and target port values
|
||||||
|
t.Run("http route with failover policy - virtual ports", func(t *testing.T) {
|
||||||
|
for _, parentRefPort := range []string{"http", "8080"} {
|
||||||
|
for _, backendRefPortFoo := range []string{"http", "9090", ""} {
|
||||||
|
for _, backendRefPortBar := range []string{"http", "9091", ""} {
|
||||||
|
for _, configKeyPortFoo := range []string{"http", "9090", ""} {
|
||||||
|
t.Run(fmt.Sprintf("%v, %v, %v, %v", parentRefPort, backendRefPortFoo, backendRefPortBar, configKeyPortFoo), func(t *testing.T) {
|
||||||
|
apiServiceData := &pbcatalog.Service{
|
||||||
|
Workloads: &pbcatalog.WorkloadSelector{
|
||||||
|
Prefixes: []string{"api-"},
|
||||||
|
},
|
||||||
|
Ports: []*pbcatalog.ServicePort{
|
||||||
|
{TargetPort: "mesh", VirtualPort: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
||||||
|
{TargetPort: "http", VirtualPort: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fooServiceData := &pbcatalog.Service{
|
||||||
|
Workloads: &pbcatalog.WorkloadSelector{
|
||||||
|
Prefixes: []string{"foo-"},
|
||||||
|
},
|
||||||
|
Ports: []*pbcatalog.ServicePort{
|
||||||
|
{TargetPort: "mesh", VirtualPort: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
||||||
|
{TargetPort: "http", VirtualPort: 9090, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
barServiceData := &pbcatalog.Service{
|
||||||
|
Workloads: &pbcatalog.WorkloadSelector{
|
||||||
|
Prefixes: []string{"bar-"},
|
||||||
|
},
|
||||||
|
Ports: []*pbcatalog.ServicePort{
|
||||||
|
{TargetPort: "mesh", VirtualPort: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
|
||||||
|
{TargetPort: "http", VirtualPort: 9091, Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRoute1 := &pbmesh.HTTPRoute{
|
||||||
|
ParentRefs: []*pbmesh.ParentReference{
|
||||||
|
newParentRef(newRef(pbcatalog.ServiceType, "api", tenancy), parentRefPort),
|
||||||
|
},
|
||||||
|
Rules: []*pbmesh.HTTPRouteRule{{
|
||||||
|
Matches: []*pbmesh.HTTPRouteMatch{{
|
||||||
|
Path: &pbmesh.HTTPPathMatch{
|
||||||
|
Type: pbmesh.PathMatchType_PATH_MATCH_TYPE_PREFIX,
|
||||||
|
Value: "/",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
BackendRefs: []*pbmesh.HTTPBackendRef{{
|
||||||
|
BackendRef: newBackendRef(fooServiceRef, backendRefPortFoo, ""),
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
failoverPolicy := &pbcatalog.FailoverPolicy{
|
||||||
|
Config: &pbcatalog.FailoverConfig{
|
||||||
|
Destinations: []*pbcatalog.FailoverDestination{
|
||||||
|
{Ref: barServiceRef}, // port is not supported in non-ported config
|
||||||
|
{Ref: deadServiceRef}, // no service
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Test ported config if used in test case
|
||||||
|
if configKeyPortFoo != "" {
|
||||||
|
failoverPolicy = &pbcatalog.FailoverPolicy{
|
||||||
|
PortConfigs: map[string]*pbcatalog.FailoverConfig{
|
||||||
|
configKeyPortFoo: {
|
||||||
|
Destinations: []*pbcatalog.FailoverDestination{
|
||||||
|
{Ref: barServiceRef, Port: backendRefPortBar},
|
||||||
|
{Ref: deadServiceRef}, // no service
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectedPortFailoverConfig := &pbmesh.ComputedFailoverConfig{
|
||||||
|
Destinations: []*pbmesh.ComputedFailoverDestination{
|
||||||
|
{BackendTarget: backendName("bar", "http")},
|
||||||
|
// we skip the dead route
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
related := loader.NewRelatedResources().
|
||||||
|
AddComputedRoutesIDs(apiComputedRoutesID).
|
||||||
|
AddResources(
|
||||||
|
newService("api", apiServiceData),
|
||||||
|
newService("foo", fooServiceData),
|
||||||
|
newService("bar", barServiceData),
|
||||||
|
newHTTPRoute("api-http-route1", httpRoute1),
|
||||||
|
newFailPolicy("foo", failoverPolicy),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Same result as non-virtual-port variant of test
|
||||||
|
expect := []*ComputedRoutesResult{{
|
||||||
|
ID: apiComputedRoutesID,
|
||||||
|
OwnerID: apiServiceID,
|
||||||
|
Data: &pbmesh.ComputedRoutes{
|
||||||
|
BoundReferences: []*pbresource.Reference{
|
||||||
|
newRef(pbcatalog.FailoverPolicyType, "foo", tenancy),
|
||||||
|
apiServiceRef,
|
||||||
|
barServiceRef,
|
||||||
|
fooServiceRef,
|
||||||
|
newRef(pbmesh.HTTPRouteType, "api-http-route1", tenancy),
|
||||||
|
},
|
||||||
|
PortedConfigs: map[string]*pbmesh.ComputedPortRoutes{
|
||||||
|
"http": {
|
||||||
|
Config: &pbmesh.ComputedPortRoutes_Http{
|
||||||
|
Http: &pbmesh.ComputedHTTPRoute{
|
||||||
|
Rules: []*pbmesh.ComputedHTTPRouteRule{
|
||||||
|
{
|
||||||
|
Matches: defaultHTTPRouteMatches(),
|
||||||
|
BackendRefs: []*pbmesh.ComputedHTTPBackendRef{{
|
||||||
|
BackendTarget: backendName("foo", "http"),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Matches: defaultHTTPRouteMatches(),
|
||||||
|
BackendRefs: []*pbmesh.ComputedHTTPBackendRef{{
|
||||||
|
BackendTarget: types.NullRouteBackend,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ParentRef: newParentRef(apiServiceRef, "http"),
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
|
Targets: map[string]*pbmesh.BackendTargetDetails{
|
||||||
|
backendName("foo", "http"): {
|
||||||
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
|
||||||
|
MeshPort: "mesh",
|
||||||
|
BackendRef: newBackendRef(fooServiceRef, "http", ""),
|
||||||
|
FailoverConfig: expectedPortFailoverConfig,
|
||||||
|
DestinationConfig: defaultDestConfig(),
|
||||||
|
},
|
||||||
|
// Indirect target with unspecified port gets parent ref port
|
||||||
|
backendName("bar", "http"): {
|
||||||
|
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_INDIRECT,
|
||||||
|
MeshPort: "mesh",
|
||||||
|
BackendRef: newBackendRef(barServiceRef, "http", ""),
|
||||||
|
DestinationConfig: defaultDestConfig(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
run(t, related, expect, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/internal/mesh/internal/controllers/routes/loader"
|
"github.com/hashicorp/consul/internal/mesh/internal/controllers/routes/loader"
|
||||||
"github.com/hashicorp/consul/internal/mesh/internal/types"
|
"github.com/hashicorp/consul/internal/mesh/internal/types"
|
||||||
"github.com/hashicorp/consul/internal/resource"
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
|
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
||||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ func computeNewRouteRefConditions(
|
||||||
|
|
||||||
// TODO(rb): handle port numbers here too if we are allowing those instead of the name?
|
// TODO(rb): handle port numbers here too if we are allowing those instead of the name?
|
||||||
|
|
||||||
|
usedParentTargetPorts := make(map[string]any)
|
||||||
for _, parentRef := range parentRefs {
|
for _, parentRef := range parentRefs {
|
||||||
if parentRef.Ref == nil || !resource.EqualType(parentRef.Ref.Type, pbcatalog.ServiceType) {
|
if parentRef.Ref == nil || !resource.EqualType(parentRef.Ref.Type, pbcatalog.ServiceType) {
|
||||||
continue // not possible due to xRoute validation
|
continue // not possible due to xRoute validation
|
||||||
|
@ -58,11 +60,17 @@ func computeNewRouteRefConditions(
|
||||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
hasMesh = true
|
hasMesh = true
|
||||||
}
|
}
|
||||||
if port.TargetPort == parentRef.Port {
|
if port.MatchesPortId(parentRef.Port) {
|
||||||
found = true
|
found = true
|
||||||
|
portRef := fmt.Sprintf("%s:%s", resource.ReferenceToString(parentRef.Ref), port.TargetPort)
|
||||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
usingMesh = true
|
usingMesh = true
|
||||||
}
|
}
|
||||||
|
if _, ok := usedParentTargetPorts[portRef]; ok {
|
||||||
|
conditions = append(conditions, ConditionConflictParentRefPort(parentRef.Ref, port.TargetPort))
|
||||||
|
} else {
|
||||||
|
usedParentTargetPorts[portRef] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
|
@ -80,6 +88,7 @@ func computeNewRouteRefConditions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usedBackendTargetPorts := make(map[string]any)
|
||||||
for _, backendRef := range backendRefs {
|
for _, backendRef := range backendRefs {
|
||||||
if backendRef.Ref == nil || !resource.EqualType(backendRef.Ref.Type, pbcatalog.ServiceType) {
|
if backendRef.Ref == nil || !resource.EqualType(backendRef.Ref.Type, pbcatalog.ServiceType) {
|
||||||
continue // not possible due to xRoute validation
|
continue // not possible due to xRoute validation
|
||||||
|
@ -95,11 +104,17 @@ func computeNewRouteRefConditions(
|
||||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
hasMesh = true
|
hasMesh = true
|
||||||
}
|
}
|
||||||
if port.TargetPort == backendRef.Port {
|
if port.MatchesPortId(backendRef.Port) {
|
||||||
found = true
|
found = true
|
||||||
|
portRef := fmt.Sprintf("%s:%s", resource.ReferenceToString(backendRef.Ref), port.TargetPort)
|
||||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
usingMesh = true
|
usingMesh = true
|
||||||
}
|
}
|
||||||
|
if _, ok := usedBackendTargetPorts[portRef]; ok {
|
||||||
|
conditions = append(conditions, ConditionConflictBackendRefPort(backendRef.Ref, port.TargetPort))
|
||||||
|
} else {
|
||||||
|
usedBackendTargetPorts[portRef] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
|
|
|
@ -24,12 +24,18 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
types.Register(registry)
|
types.Register(registry)
|
||||||
catalog.RegisterTypes(registry)
|
catalog.RegisterTypes(registry)
|
||||||
|
|
||||||
newService := func(name string, ports map[string]pbcatalog.Protocol) *types.DecodedService {
|
type protocolAndVirtualPort struct {
|
||||||
|
protocol pbcatalog.Protocol
|
||||||
|
virtualPort uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
newService := func(name string, ports map[string]protocolAndVirtualPort) *types.DecodedService {
|
||||||
var portSlice []*pbcatalog.ServicePort
|
var portSlice []*pbcatalog.ServicePort
|
||||||
for name, proto := range ports {
|
for targetPort, pv := range ports {
|
||||||
portSlice = append(portSlice, &pbcatalog.ServicePort{
|
portSlice = append(portSlice, &pbcatalog.ServicePort{
|
||||||
TargetPort: name,
|
TargetPort: targetPort,
|
||||||
Protocol: proto,
|
VirtualPort: pv.virtualPort,
|
||||||
|
Protocol: pv.protocol,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
svc := rtest.Resource(pbcatalog.ServiceType, name).
|
svc := rtest.Resource(pbcatalog.ServiceType, name).
|
||||||
|
@ -61,8 +67,8 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service but no mesh port", func(t *testing.T) {
|
t.Run("with service but no mesh port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
||||||
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), ""),
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), ""),
|
||||||
|
@ -74,9 +80,9 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service but using mesh port", func(t *testing.T) {
|
t.Run("with service but using mesh port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
||||||
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "mesh"),
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "mesh"),
|
||||||
|
@ -89,9 +95,9 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service and using missing port", func(t *testing.T) {
|
t.Run("with service and using missing port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
||||||
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "web"),
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "web"),
|
||||||
|
@ -103,10 +109,26 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("with service and using duplicate port", func(t *testing.T) {
|
||||||
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
|
}))
|
||||||
|
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
||||||
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "http"),
|
||||||
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "8080"),
|
||||||
|
}, nil)
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
prototest.AssertContainsElement(t, got, ConditionConflictParentRefPort(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()),
|
||||||
|
"http",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("with service and using empty port", func(t *testing.T) {
|
t.Run("with service and using empty port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
||||||
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), ""),
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), ""),
|
||||||
|
@ -115,9 +137,9 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service and using correct port", func(t *testing.T) {
|
t.Run("with service and using correct port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
got := computeNewRouteRefConditions(sg, []*pbmesh.ParentReference{
|
||||||
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "http"),
|
newParentRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "http"),
|
||||||
|
@ -139,8 +161,8 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service but no mesh port", func(t *testing.T) {
|
t.Run("with service but no mesh port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
||||||
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "", ""),
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "", ""),
|
||||||
|
@ -152,9 +174,9 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service but using mesh port", func(t *testing.T) {
|
t.Run("with service but using mesh port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
||||||
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "mesh", ""),
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "mesh", ""),
|
||||||
|
@ -167,9 +189,9 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service and using missing port", func(t *testing.T) {
|
t.Run("with service and using missing port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
||||||
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "web", ""),
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "web", ""),
|
||||||
|
@ -181,10 +203,26 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("with service and using duplicate port", func(t *testing.T) {
|
||||||
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
|
}))
|
||||||
|
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
||||||
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "http", ""),
|
||||||
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "8080", ""),
|
||||||
|
})
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
prototest.AssertContainsElement(t, got, ConditionConflictBackendRefPort(
|
||||||
|
newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()),
|
||||||
|
"http",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("with service and using empty port", func(t *testing.T) {
|
t.Run("with service and using empty port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
||||||
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "", ""),
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "", ""),
|
||||||
|
@ -193,9 +231,9 @@ func TestComputeNewRouteRefConditions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with service and using correct port", func(t *testing.T) {
|
t.Run("with service and using correct port", func(t *testing.T) {
|
||||||
sg := newTestServiceGetter(newService("api", map[string]pbcatalog.Protocol{
|
sg := newTestServiceGetter(newService("api", map[string]protocolAndVirtualPort{
|
||||||
"http": pbcatalog.Protocol_PROTOCOL_HTTP,
|
"http": {pbcatalog.Protocol_PROTOCOL_HTTP, 8080},
|
||||||
"mesh": pbcatalog.Protocol_PROTOCOL_MESH,
|
"mesh": {pbcatalog.Protocol_PROTOCOL_MESH, 20000},
|
||||||
}))
|
}))
|
||||||
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
got := computeNewRouteRefConditions(sg, nil, []*pbmesh.BackendReference{
|
||||||
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "http", ""),
|
newBackendRef(newRef(pbcatalog.ServiceType, "api", resource.DefaultNamespacedTenancy()), "http", ""),
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/internal/resource"
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
catalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,10 +29,16 @@ const (
|
||||||
ParentRefUsingMeshPortReason = "ParentRefUsingMeshPort"
|
ParentRefUsingMeshPortReason = "ParentRefUsingMeshPort"
|
||||||
BackendRefUsingMeshPortReason = "BackendRefUsingMeshPort"
|
BackendRefUsingMeshPortReason = "BackendRefUsingMeshPort"
|
||||||
|
|
||||||
UnknownParentRefPortReason = "UnknownParentRefPort"
|
UnknownParentRefPortReason = "UnknownParentRefPort"
|
||||||
UnknownBackendRefPortReason = "UnknownBackendRefPort"
|
UnknownBackendRefPortReason = "UnknownBackendRefPort"
|
||||||
|
UnknownDestinationPortReason = "UnknownDestinationPort"
|
||||||
|
ConflictParentRefPortReason = "ConflictParentRefPort"
|
||||||
|
ConflictBackendRefPortReason = "ConflictBackendRefPort"
|
||||||
|
ConflictDestinationPortReason = "ConflictDestinationPort"
|
||||||
|
|
||||||
ConflictNotBoundToParentRefReason = "ConflictNotBoundToParentRef"
|
ConflictNotBoundToParentRefReason = "ConflictNotBoundToParentRef"
|
||||||
|
|
||||||
|
DestinationServiceNotFoundReason = "DestinationServiceNotFound"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -153,16 +160,79 @@ func conditionUnknownRefPort(ref *pbresource.Reference, port string, forBackend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConditionConflictParentRefPort(ref *pbresource.Reference, port string) *pbresource.Condition {
|
||||||
|
return conditionConflictRefPort(ref, port, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConditionConflictBackendRefPort(ref *pbresource.Reference, port string) *pbresource.Condition {
|
||||||
|
return conditionConflictRefPort(ref, port, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionConflictRefPort(ref *pbresource.Reference, port string, forBackend bool) *pbresource.Condition {
|
||||||
|
reason := ConflictParentRefPortReason
|
||||||
|
short := "parent"
|
||||||
|
if forBackend {
|
||||||
|
reason = ConflictBackendRefPortReason
|
||||||
|
short = "backend"
|
||||||
|
}
|
||||||
|
return &pbresource.Condition{
|
||||||
|
Type: StatusConditionAccepted,
|
||||||
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
|
Reason: reason,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"multiple %s refs found for service %q on target port %q",
|
||||||
|
short,
|
||||||
|
resource.ReferenceToString(ref),
|
||||||
|
port,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ConditionConflictNotBoundToParentRef(ref *pbresource.Reference, port string, realType *pbresource.Type) *pbresource.Condition {
|
func ConditionConflictNotBoundToParentRef(ref *pbresource.Reference, port string, realType *pbresource.Type) *pbresource.Condition {
|
||||||
return &pbresource.Condition{
|
return &pbresource.Condition{
|
||||||
Type: StatusConditionAccepted,
|
Type: StatusConditionAccepted,
|
||||||
State: pbresource.Condition_STATE_FALSE,
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
Reason: ConflictNotBoundToParentRefReason,
|
Reason: ConflictNotBoundToParentRefReason,
|
||||||
Message: fmt.Sprintf(
|
Message: fmt.Sprintf(
|
||||||
"Existing routes of type %q are bound to parent ref %q on port %q preventing this from binding",
|
"existing routes of type %q are bound to parent ref %q on port %q preventing this from binding",
|
||||||
resource.TypeToString(realType),
|
resource.TypeToString(realType),
|
||||||
resource.ReferenceToString(ref),
|
resource.ReferenceToString(ref),
|
||||||
port,
|
port,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConditionDestinationServiceNotFound(serviceRef *pbresource.Reference) *pbresource.Condition {
|
||||||
|
return &pbresource.Condition{
|
||||||
|
Type: StatusConditionAccepted,
|
||||||
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
|
Reason: DestinationServiceNotFoundReason,
|
||||||
|
Message: fmt.Sprintf("service %q does not exist.", resource.ReferenceToString(serviceRef)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConditionUnknownDestinationPort(serviceRef *pbresource.Reference, port string) *pbresource.Condition {
|
||||||
|
return &pbresource.Condition{
|
||||||
|
Type: StatusConditionAccepted,
|
||||||
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
|
Reason: UnknownDestinationPortReason,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"port is not defined on service: %s on %s",
|
||||||
|
port,
|
||||||
|
resource.ReferenceToString(serviceRef),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConditionConflictDestinationPort(serviceRef *pbresource.Reference, port *catalog.ServicePort) *pbresource.Condition {
|
||||||
|
return &pbresource.Condition{
|
||||||
|
Type: StatusConditionAccepted,
|
||||||
|
State: pbresource.Condition_STATE_FALSE,
|
||||||
|
Reason: ConflictDestinationPortReason,
|
||||||
|
Message: fmt.Sprintf(
|
||||||
|
"multiple configs found for port on destination service: %s on %s",
|
||||||
|
port.ToPrintableString(),
|
||||||
|
resource.ReferenceToString(serviceRef),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ func (b *Builder) buildDestination(
|
||||||
var virtualPortNumber uint32
|
var virtualPortNumber uint32
|
||||||
if destination.Explicit == nil {
|
if destination.Explicit == nil {
|
||||||
for _, port := range destination.Service.Data.Ports {
|
for _, port := range destination.Service.Data.Ports {
|
||||||
if port.TargetPort == cpr.ParentRef.Port {
|
if port.MatchesPortId(cpr.ParentRef.Port) {
|
||||||
virtualPortNumber = port.VirtualPort
|
virtualPortNumber = port.VirtualPort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,7 +299,7 @@ func (r *reconciler) workloadPortProtocolsFromService(
|
||||||
inheritedProtocol := pbcatalog.Protocol_PROTOCOL_UNSPECIFIED
|
inheritedProtocol := pbcatalog.Protocol_PROTOCOL_UNSPECIFIED
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
// Find workload's port as the target port.
|
// Find workload's port as the target port.
|
||||||
svcPort := svc.GetData().FindServicePort(portName)
|
svcPort := svc.GetData().FindTargetPort(portName)
|
||||||
|
|
||||||
// If this service doesn't select this port, go to the next service.
|
// If this service doesn't select this port, go to the next service.
|
||||||
if svcPort == nil {
|
if svcPort == nil {
|
||||||
|
|
|
@ -151,13 +151,13 @@ func (f *Fetcher) FetchComputedExplicitDestinationsData(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the desired port exists on the service and skip it doesn't.
|
// Check if the desired port exists on the service and skip it doesn't.
|
||||||
if svc.GetData().FindServicePort(dest.DestinationPort) == nil {
|
if svc.GetData().FindPortByID(dest.DestinationPort) == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// No destination port should point to a port with "mesh" protocol,
|
// No destination port should point to a port with "mesh" protocol,
|
||||||
// so check if destination port has the mesh protocol and skip it if it does.
|
// so check if destination port has the mesh protocol and skip it if it does.
|
||||||
if svc.GetData().FindServicePort(dest.DestinationPort).GetProtocol() == pbcatalog.Protocol_PROTOCOL_MESH {
|
if svc.GetData().FindPortByID(dest.DestinationPort).GetProtocol() == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/internal/resource"
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
|
||||||
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
|
||||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
)
|
)
|
||||||
|
@ -214,6 +216,48 @@ func validateDestinationPolicy(res *DecodedDestinationPolicy) error {
|
||||||
return merr
|
return merr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SimplifyDestinationPolicy normalizes port references in the DestinationPolicy
|
||||||
|
// using the provided Service.
|
||||||
|
func SimplifyDestinationPolicy(svc *pbcatalog.Service, policy *pbmesh.DestinationPolicy) *pbmesh.DestinationPolicy {
|
||||||
|
if policy == nil {
|
||||||
|
panic("destination policy is required")
|
||||||
|
}
|
||||||
|
if svc == nil {
|
||||||
|
panic("service is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy so we can edit it.
|
||||||
|
dup := proto.Clone(policy)
|
||||||
|
policy = dup.(*pbmesh.DestinationPolicy)
|
||||||
|
|
||||||
|
if policy.PortConfigs == nil {
|
||||||
|
policy.PortConfigs = make(map[string]*pbmesh.DestinationConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize all port configs to use the target port of the corresponding service port.
|
||||||
|
normalizedPortConfigs := make(map[string]*pbmesh.DestinationConfig)
|
||||||
|
for port, pc := range policy.PortConfigs {
|
||||||
|
svcPort := svc.FindPortByID(port)
|
||||||
|
|
||||||
|
if svcPort != nil {
|
||||||
|
if _, ok := normalizedPortConfigs[svcPort.TargetPort]; ok {
|
||||||
|
// This is a duplicate virtual and target port mapping that will be reported as a status condition.
|
||||||
|
// Only update if this is the "canonical" mapping; otherwise, it's virtual, and we should ignore.
|
||||||
|
if port != svcPort.TargetPort {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizedPortConfigs[svcPort.TargetPort] = pc
|
||||||
|
}
|
||||||
|
// Else this is an invalid reference that will be reported as a status condition.
|
||||||
|
// Drop for safety and simpler output.
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.PortConfigs = normalizedPortConfigs
|
||||||
|
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
func aclReadHookDestinationPolicy(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.ID, _ *pbresource.Resource) error {
|
func aclReadHookDestinationPolicy(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.ID, _ *pbresource.Resource) error {
|
||||||
// DestinationPolicy is name-aligned with Service
|
// DestinationPolicy is name-aligned with Service
|
||||||
serviceName := id.Name
|
serviceName := id.Name
|
||||||
|
|
|
@ -99,7 +99,7 @@ func validateDestinations(res *DecodedDestinations) error {
|
||||||
merr = multierror.Append(merr, refErr)
|
merr = multierror.Append(merr, refErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if portErr := catalog.ValidatePortName(dest.DestinationPort); portErr != nil {
|
if portErr := catalog.ValidateServicePortID(dest.DestinationPort); portErr != nil {
|
||||||
merr = multierror.Append(merr, wrapDestErr(resource.ErrInvalidField{
|
merr = multierror.Append(merr, wrapDestErr(resource.ErrInvalidField{
|
||||||
Name: "destination_port",
|
Name: "destination_port",
|
||||||
Wrapped: portErr,
|
Wrapped: portErr,
|
||||||
|
|
|
@ -83,8 +83,11 @@ type FailoverPolicy struct {
|
||||||
|
|
||||||
// Config defines failover for any named port not present in PortConfigs.
|
// Config defines failover for any named port not present in PortConfigs.
|
||||||
Config *FailoverConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
|
Config *FailoverConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
|
||||||
// PortConfigs defines failover for a specific port on this service and takes
|
// PortConfigs defines failover for a specific port on a service and takes
|
||||||
// precedence over Config.
|
// precedence over Config.
|
||||||
|
//
|
||||||
|
// For more details on potential values of the service port identifier key,
|
||||||
|
// see documentation for Service.ServicePort.
|
||||||
PortConfigs map[string]*FailoverConfig `protobuf:"bytes,2,rep,name=port_configs,json=portConfigs,proto3" json:"port_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
PortConfigs map[string]*FailoverConfig `protobuf:"bytes,2,rep,name=port_configs,json=portConfigs,proto3" json:"port_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +221,10 @@ type FailoverDestination struct {
|
||||||
|
|
||||||
// This must be a Service.
|
// This must be a Service.
|
||||||
Ref *pbresource.Reference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"`
|
Ref *pbresource.Reference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"`
|
||||||
|
// Port is the port of the destination service.
|
||||||
|
//
|
||||||
|
// For more details on potential values of the service port identifier key,
|
||||||
|
// see documentation for Service.ServicePort.
|
||||||
// TODO: what should an empty port mean?
|
// TODO: what should an empty port mean?
|
||||||
Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
|
Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
|
||||||
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
||||||
|
|
|
@ -15,8 +15,11 @@ message FailoverPolicy {
|
||||||
// Config defines failover for any named port not present in PortConfigs.
|
// Config defines failover for any named port not present in PortConfigs.
|
||||||
FailoverConfig config = 1;
|
FailoverConfig config = 1;
|
||||||
|
|
||||||
// PortConfigs defines failover for a specific port on this service and takes
|
// PortConfigs defines failover for a specific port on a service and takes
|
||||||
// precedence over Config.
|
// precedence over Config.
|
||||||
|
//
|
||||||
|
// For more details on potential values of the service port identifier key,
|
||||||
|
// see documentation for Service.ServicePort.
|
||||||
map<string, FailoverConfig> port_configs = 2;
|
map<string, FailoverConfig> port_configs = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +41,11 @@ message FailoverConfig {
|
||||||
message FailoverDestination {
|
message FailoverDestination {
|
||||||
// This must be a Service.
|
// This must be a Service.
|
||||||
hashicorp.consul.resource.Reference ref = 1;
|
hashicorp.consul.resource.Reference ref = 1;
|
||||||
|
|
||||||
|
// Port is the port of the destination service.
|
||||||
|
//
|
||||||
|
// For more details on potential values of the service port identifier key,
|
||||||
|
// see documentation for Service.ServicePort.
|
||||||
// TODO: what should an empty port mean?
|
// TODO: what should an empty port mean?
|
||||||
string port = 2;
|
string port = 2;
|
||||||
string datacenter = 3;
|
string datacenter = 3;
|
||||||
|
|
|
@ -92,6 +92,13 @@ func (x *Service) GetVirtualIps() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServicePort declares a port exposed by the service that can be used in config and xRoute
|
||||||
|
// references.
|
||||||
|
//
|
||||||
|
// For outside references to a service port by string identifier (e.g. in xRoutes and xPolicies),
|
||||||
|
// there are two forms supported:
|
||||||
|
// - A numeric value exclusively indicates a ServicePort.VirtualPort
|
||||||
|
// - A non-numeric value exclusively indicates a ServicePort.TargetPort
|
||||||
type ServicePort struct {
|
type ServicePort struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|
|
@ -23,6 +23,13 @@ message Service {
|
||||||
repeated string virtual_ips = 3;
|
repeated string virtual_ips = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServicePort declares a port exposed by the service that can be used in config and xRoute
|
||||||
|
// references.
|
||||||
|
//
|
||||||
|
// For outside references to a service port by string identifier (e.g. in xRoutes and xPolicies),
|
||||||
|
// there are two forms supported:
|
||||||
|
// - A numeric value exclusively indicates a ServicePort.VirtualPort
|
||||||
|
// - A non-numeric value exclusively indicates a ServicePort.TargetPort
|
||||||
message ServicePort {
|
message ServicePort {
|
||||||
// virtual_port is the port that could only be used when transparent
|
// virtual_port is the port that could only be used when transparent
|
||||||
// proxy is used alongside a virtual IP or a virtual DNS address.
|
// proxy is used alongside a virtual IP or a virtual DNS address.
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
|
|
||||||
package catalogv2beta1
|
package catalogv2beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
func (s *Service) IsMeshEnabled() bool {
|
func (s *Service) IsMeshEnabled() bool {
|
||||||
for _, port := range s.GetPorts() {
|
for _, port := range s.GetPorts() {
|
||||||
if port.Protocol == Protocol_PROTOCOL_MESH {
|
if port.Protocol == Protocol_PROTOCOL_MESH {
|
||||||
|
@ -12,11 +17,104 @@ func (s *Service) IsMeshEnabled() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FindServicePort(name string) *ServicePort {
|
// FindTargetPort finds a ServicePort by its TargetPort value.
|
||||||
|
//
|
||||||
|
// Unlike FindPortByID, it will match a numeric TargetPort value. This is useful when
|
||||||
|
// looking up a service port by a workload port value, or when the data is known to
|
||||||
|
// be normalized to canonical target port values (e.g. computed routes).
|
||||||
|
func (s *Service) FindTargetPort(targetPort string) *ServicePort {
|
||||||
|
if s == nil || targetPort == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, port := range s.GetPorts() {
|
for _, port := range s.GetPorts() {
|
||||||
if port.TargetPort == name {
|
if port.TargetPort == targetPort {
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindPortByID finds a ServicePort by its VirtualPort or TargetPort value.
|
||||||
|
//
|
||||||
|
// Note that this will not match a target port if the given value is numeric.
|
||||||
|
// See Service.ServicePort doc for more information on how port IDs are matched.
|
||||||
|
func (s *Service) FindPortByID(id string) *ServicePort {
|
||||||
|
if s == nil || id == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a port reference is numeric, it must be considered a virtual port.
|
||||||
|
// See ServicePort doc for more information.
|
||||||
|
if p, ok := toVirtualPort(id); ok {
|
||||||
|
for _, port := range s.GetPorts() {
|
||||||
|
if int(port.VirtualPort) == p {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, port := range s.GetPorts() {
|
||||||
|
if port.TargetPort == id {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesPortId returns true if the given port ID is non-empty and matches the virtual
|
||||||
|
// or target port of the given ServicePort. See ServicePort doc for more information on
|
||||||
|
// how port IDs are matched.
|
||||||
|
//
|
||||||
|
// Note that this function does not validate the provided port ID. Configured service
|
||||||
|
// ports should be validated on write, prior to use of this function, which means any
|
||||||
|
// matching value is implicitly valid.
|
||||||
|
func (sp *ServicePort) MatchesPortId(id string) bool {
|
||||||
|
if sp == nil || id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a port reference is numeric, it must be considered a virtual port.
|
||||||
|
// See ServicePort doc for more information.
|
||||||
|
if p, ok := toVirtualPort(id); ok {
|
||||||
|
if int(sp.VirtualPort) == p {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if sp.TargetPort == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// VirtualPortStr is a convenience helper for checking the virtual port against a port ID in config
|
||||||
|
// (e.g. keys in FailoverPolicy.PortConfigs). It returns the string representation of the virtual port.
|
||||||
|
func (sp *ServicePort) VirtualPortStr() string {
|
||||||
|
if sp == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", sp.VirtualPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *ServicePort) ToPrintableString() string {
|
||||||
|
if sp == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
if sp.VirtualPort > 0 {
|
||||||
|
return fmt.Sprintf("%s (virtual %d)", sp.TargetPort, sp.VirtualPort)
|
||||||
|
}
|
||||||
|
return sp.TargetPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// isVirtualPort returns the numeric virtual port value and true if the given port string is fully numeric.
|
||||||
|
// Otherwise, returns 0 and false. See ServicePort doc for more information.
|
||||||
|
func toVirtualPort(port string) (int, bool) {
|
||||||
|
if p, err := strconv.Atoi(port); err == nil && p > 0 {
|
||||||
|
return p, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
|
@ -62,17 +62,19 @@ func TestServiceIsMeshEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindServicePort(t *testing.T) {
|
func TestFindPort(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
service *Service
|
service *Service
|
||||||
port string
|
port string
|
||||||
exp *ServicePort
|
expById *ServicePort
|
||||||
|
expByTargetPort *ServicePort
|
||||||
}{
|
}{
|
||||||
"nil": {service: nil, port: "foo", exp: nil},
|
"nil": {service: nil, port: "foo", expById: nil, expByTargetPort: nil},
|
||||||
"no ports": {
|
"no ports": {
|
||||||
service: &Service{},
|
service: &Service{},
|
||||||
port: "foo",
|
port: "foo",
|
||||||
exp: nil,
|
expById: nil,
|
||||||
|
expByTargetPort: nil,
|
||||||
},
|
},
|
||||||
"non-existing port": {
|
"non-existing port": {
|
||||||
service: &Service{
|
service: &Service{
|
||||||
|
@ -87,8 +89,9 @@ func TestFindServicePort(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
port: "not-found",
|
port: "not-found",
|
||||||
exp: nil,
|
expById: nil,
|
||||||
|
expByTargetPort: nil,
|
||||||
},
|
},
|
||||||
"existing port": {
|
"existing port": {
|
||||||
service: &Service{
|
service: &Service{
|
||||||
|
@ -108,16 +111,98 @@ func TestFindServicePort(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
port: "bar",
|
port: "bar",
|
||||||
exp: &ServicePort{
|
expById: &ServicePort{
|
||||||
TargetPort: "bar",
|
TargetPort: "bar",
|
||||||
Protocol: Protocol_PROTOCOL_TCP,
|
Protocol: Protocol_PROTOCOL_TCP,
|
||||||
},
|
},
|
||||||
|
expByTargetPort: &ServicePort{
|
||||||
|
TargetPort: "bar",
|
||||||
|
Protocol: Protocol_PROTOCOL_TCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"existing port by virtual port": {
|
||||||
|
service: &Service{
|
||||||
|
Ports: []*ServicePort{
|
||||||
|
{
|
||||||
|
TargetPort: "foo",
|
||||||
|
VirtualPort: 8080,
|
||||||
|
Protocol: Protocol_PROTOCOL_HTTP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TargetPort: "bar",
|
||||||
|
VirtualPort: 8081,
|
||||||
|
Protocol: Protocol_PROTOCOL_TCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TargetPort: "baz",
|
||||||
|
VirtualPort: 8081,
|
||||||
|
Protocol: Protocol_PROTOCOL_MESH,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
port: "8081",
|
||||||
|
expById: &ServicePort{
|
||||||
|
TargetPort: "bar",
|
||||||
|
VirtualPort: 8081,
|
||||||
|
Protocol: Protocol_PROTOCOL_TCP,
|
||||||
|
},
|
||||||
|
expByTargetPort: nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, c := range cases {
|
for name, c := range cases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
require.Equal(t, c.exp, c.service.FindServicePort(c.port))
|
require.Equal(t, c.expById, c.service.FindPortByID(c.port))
|
||||||
|
require.Equal(t, c.expByTargetPort, c.service.FindTargetPort(c.port))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchesPortId(t *testing.T) {
|
||||||
|
testPort := &ServicePort{VirtualPort: 8080, TargetPort: "http"}
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
port *ServicePort
|
||||||
|
id string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
"nil": {port: nil, id: "foo", expected: false},
|
||||||
|
"empty": {port: testPort, id: "", expected: false},
|
||||||
|
"non-existing virtual port": {
|
||||||
|
port: testPort,
|
||||||
|
id: "9090",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
"non-existing target port": {
|
||||||
|
port: testPort,
|
||||||
|
id: "other-port",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
"existing virtual port": {
|
||||||
|
port: testPort,
|
||||||
|
id: "8080",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
"existing target port": {
|
||||||
|
port: testPort,
|
||||||
|
id: "http",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
"virtual and target mismatch": {
|
||||||
|
port: &ServicePort{VirtualPort: 8080, TargetPort: "9090"},
|
||||||
|
id: "9090",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
"virtual and target match": {
|
||||||
|
port: &ServicePort{VirtualPort: 9090, TargetPort: "9090"},
|
||||||
|
id: "9090",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
require.Equal(t, c.expected, c.port.MatchesPortId(c.id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,9 @@ type ParentReference struct {
|
||||||
// For east/west this is the name of the Consul Service port to direct traffic to
|
// For east/west this is the name of the Consul Service port to direct traffic to
|
||||||
// or empty to imply all.
|
// or empty to imply all.
|
||||||
// For north/south this is TBD.
|
// For north/south this is TBD.
|
||||||
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for
|
||||||
|
// Service.ServicePort.
|
||||||
Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
|
Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +97,10 @@ type BackendReference struct {
|
||||||
Ref *pbresource.Reference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"`
|
Ref *pbresource.Reference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"`
|
||||||
// For east/west this is the name of the Consul Service port to direct traffic to
|
// For east/west this is the name of the Consul Service port to direct traffic to
|
||||||
// or empty to imply using the same value as the parent ref.
|
// or empty to imply using the same value as the parent ref.
|
||||||
//
|
|
||||||
// For north/south this is TBD.
|
// For north/south this is TBD.
|
||||||
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for
|
||||||
|
// Service.ServicePort.
|
||||||
Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
|
Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"`
|
||||||
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@ message ParentReference {
|
||||||
// For east/west this is the name of the Consul Service port to direct traffic to
|
// For east/west this is the name of the Consul Service port to direct traffic to
|
||||||
// or empty to imply all.
|
// or empty to imply all.
|
||||||
// For north/south this is TBD.
|
// For north/south this is TBD.
|
||||||
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for
|
||||||
|
// Service.ServicePort.
|
||||||
string port = 2;
|
string port = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +28,10 @@ message BackendReference {
|
||||||
|
|
||||||
// For east/west this is the name of the Consul Service port to direct traffic to
|
// For east/west this is the name of the Consul Service port to direct traffic to
|
||||||
// or empty to imply using the same value as the parent ref.
|
// or empty to imply using the same value as the parent ref.
|
||||||
//
|
|
||||||
// For north/south this is TBD.
|
// For north/south this is TBD.
|
||||||
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for
|
||||||
|
// Service.ServicePort.
|
||||||
string port = 2;
|
string port = 2;
|
||||||
string datacenter = 3;
|
string datacenter = 3;
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,12 @@ type ComputedRoutes struct {
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// PortedConfigs is the map of service ports to the ComputedPortRoutes for
|
||||||
|
// those ports.
|
||||||
|
//
|
||||||
|
// The port identifier key here is always normalized to the target (workload)
|
||||||
|
// port name regardless of whether a virtual or target port identifier was
|
||||||
|
// provided in input config.
|
||||||
PortedConfigs map[string]*ComputedPortRoutes `protobuf:"bytes,1,rep,name=ported_configs,json=portedConfigs,proto3" json:"ported_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
PortedConfigs map[string]*ComputedPortRoutes `protobuf:"bytes,1,rep,name=ported_configs,json=portedConfigs,proto3" json:"ported_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||||
// BoundReferences is a slice of mixed type references of resources that were
|
// BoundReferences is a slice of mixed type references of resources that were
|
||||||
// involved in the formulation of this resource.
|
// involved in the formulation of this resource.
|
||||||
|
|
|
@ -21,6 +21,12 @@ import "pbresource/resource.proto";
|
||||||
message ComputedRoutes {
|
message ComputedRoutes {
|
||||||
option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE};
|
option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE};
|
||||||
|
|
||||||
|
// PortedConfigs is the map of service ports to the ComputedPortRoutes for
|
||||||
|
// those ports.
|
||||||
|
//
|
||||||
|
// The port identifier key here is always normalized to the target (workload)
|
||||||
|
// port name regardless of whether a virtual or target port identifier was
|
||||||
|
// provided in input config.
|
||||||
map<string, ComputedPortRoutes> ported_configs = 1;
|
map<string, ComputedPortRoutes> ported_configs = 1;
|
||||||
|
|
||||||
// BoundReferences is a slice of mixed type references of resources that were
|
// BoundReferences is a slice of mixed type references of resources that were
|
||||||
|
|
|
@ -191,8 +191,8 @@ func (HashPolicyField) EnumDescriptor() ([]byte, []int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DestinationPolicy is the destination-controlled set of defaults that
|
// DestinationPolicy is the destination-controlled set of defaults that
|
||||||
// are used when similar controls defined in an UpstreamConfig are left
|
// are used when similar controls defined in an DestinationsConfiguration are
|
||||||
// unspecified.
|
// left unspecified.
|
||||||
//
|
//
|
||||||
// Users may wish to share commonly configured settings for communicating with
|
// Users may wish to share commonly configured settings for communicating with
|
||||||
// a service in one place, but yet retain the ability to tweak those on a
|
// a service in one place, but yet retain the ability to tweak those on a
|
||||||
|
@ -205,6 +205,10 @@ type DestinationPolicy struct {
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// PortConfigs defines the destination policy for a specific port on a service.
|
||||||
|
//
|
||||||
|
// For more details on potential values of the service port identifier key,
|
||||||
|
// see documentation for Service.ServicePort.
|
||||||
PortConfigs map[string]*DestinationConfig `protobuf:"bytes,1,rep,name=port_configs,json=portConfigs,proto3" json:"port_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
PortConfigs map[string]*DestinationConfig `protobuf:"bytes,1,rep,name=port_configs,json=portConfigs,proto3" json:"port_configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import "google/protobuf/duration.proto";
|
||||||
import "pbresource/annotations.proto";
|
import "pbresource/annotations.proto";
|
||||||
|
|
||||||
// DestinationPolicy is the destination-controlled set of defaults that
|
// DestinationPolicy is the destination-controlled set of defaults that
|
||||||
// are used when similar controls defined in an UpstreamConfig are left
|
// are used when similar controls defined in an DestinationsConfiguration are
|
||||||
// unspecified.
|
// left unspecified.
|
||||||
//
|
//
|
||||||
// Users may wish to share commonly configured settings for communicating with
|
// Users may wish to share commonly configured settings for communicating with
|
||||||
// a service in one place, but yet retain the ability to tweak those on a
|
// a service in one place, but yet retain the ability to tweak those on a
|
||||||
|
@ -21,6 +21,10 @@ import "pbresource/annotations.proto";
|
||||||
message DestinationPolicy {
|
message DestinationPolicy {
|
||||||
option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE};
|
option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE};
|
||||||
|
|
||||||
|
// PortConfigs defines the destination policy for a specific port on a service.
|
||||||
|
//
|
||||||
|
// For more details on potential values of the service port identifier key,
|
||||||
|
// see documentation for Service.ServicePort.
|
||||||
map<string, DestinationConfig> port_configs = 1;
|
map<string, DestinationConfig> port_configs = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,9 @@ type Destination struct {
|
||||||
|
|
||||||
// DestinationRef is the reference to an destination service. This has to be pbcatalog.Service type.
|
// DestinationRef is the reference to an destination service. This has to be pbcatalog.Service type.
|
||||||
DestinationRef *pbresource.Reference `protobuf:"bytes,1,opt,name=destination_ref,json=destinationRef,proto3" json:"destination_ref,omitempty"`
|
DestinationRef *pbresource.Reference `protobuf:"bytes,1,opt,name=destination_ref,json=destinationRef,proto3" json:"destination_ref,omitempty"`
|
||||||
// DestinationPort is the port name of the destination service. This should be the name
|
// DestinationPort is the port of the destination service.
|
||||||
// of the service's target port.
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for Service.ServicePort.
|
||||||
DestinationPort string `protobuf:"bytes,2,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"`
|
DestinationPort string `protobuf:"bytes,2,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"`
|
||||||
// Datacenter is the datacenter for where this destination service lives.
|
// Datacenter is the datacenter for where this destination service lives.
|
||||||
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
||||||
|
|
|
@ -29,8 +29,9 @@ message Destination {
|
||||||
// DestinationRef is the reference to an destination service. This has to be pbcatalog.Service type.
|
// DestinationRef is the reference to an destination service. This has to be pbcatalog.Service type.
|
||||||
hashicorp.consul.resource.Reference destination_ref = 1;
|
hashicorp.consul.resource.Reference destination_ref = 1;
|
||||||
|
|
||||||
// DestinationPort is the port name of the destination service. This should be the name
|
// DestinationPort is the port of the destination service.
|
||||||
// of the service's target port.
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for Service.ServicePort.
|
||||||
string destination_port = 2;
|
string destination_port = 2;
|
||||||
|
|
||||||
// Datacenter is the datacenter for where this destination service lives.
|
// Datacenter is the datacenter for where this destination service lives.
|
||||||
|
|
|
@ -94,7 +94,7 @@ func (x *DestinationsConfiguration) GetConfigOverrides() []*DestinationConfigOve
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpstreamConfigOverrides allow to override destination configuration per destination_ref/port/datacenter.
|
// DestinationConfigOverrides allow to override destination configuration per destination_ref/port/datacenter.
|
||||||
// In that sense, those three fields (destination_ref, destination_port and datacenter) are treated
|
// In that sense, those three fields (destination_ref, destination_port and datacenter) are treated
|
||||||
// sort of like map keys and config is a like a map value for that key.
|
// sort of like map keys and config is a like a map value for that key.
|
||||||
type DestinationConfigOverrides struct {
|
type DestinationConfigOverrides struct {
|
||||||
|
@ -105,8 +105,11 @@ type DestinationConfigOverrides struct {
|
||||||
// DestinationRef is the reference to an destination service that this configuration applies to.
|
// DestinationRef is the reference to an destination service that this configuration applies to.
|
||||||
// This has to be pbcatalog.Service type.
|
// This has to be pbcatalog.Service type.
|
||||||
DestinationRef *pbresource.Reference `protobuf:"bytes,1,opt,name=destination_ref,json=destinationRef,proto3" json:"destination_ref,omitempty"`
|
DestinationRef *pbresource.Reference `protobuf:"bytes,1,opt,name=destination_ref,json=destinationRef,proto3" json:"destination_ref,omitempty"`
|
||||||
// DestinationPort is the port name of the destination service. This should be the name
|
// DestinationPort is the port of the destination service.
|
||||||
// of the service's target port. If not provided, this configuration will apply to all ports of an destination.
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for Service.ServicePort.
|
||||||
|
//
|
||||||
|
// If not provided, this configuration will apply to all ports of an destination.
|
||||||
DestinationPort string `protobuf:"bytes,2,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"`
|
DestinationPort string `protobuf:"bytes,2,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"`
|
||||||
// Datacenter is the datacenter for where this destination service lives.
|
// Datacenter is the datacenter for where this destination service lives.
|
||||||
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
Datacenter string `protobuf:"bytes,3,opt,name=datacenter,proto3" json:"datacenter,omitempty"`
|
||||||
|
|
|
@ -28,7 +28,7 @@ message DestinationsConfiguration {
|
||||||
repeated DestinationConfigOverrides config_overrides = 3;
|
repeated DestinationConfigOverrides config_overrides = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpstreamConfigOverrides allow to override destination configuration per destination_ref/port/datacenter.
|
// DestinationConfigOverrides allow to override destination configuration per destination_ref/port/datacenter.
|
||||||
// In that sense, those three fields (destination_ref, destination_port and datacenter) are treated
|
// In that sense, those three fields (destination_ref, destination_port and datacenter) are treated
|
||||||
// sort of like map keys and config is a like a map value for that key.
|
// sort of like map keys and config is a like a map value for that key.
|
||||||
message DestinationConfigOverrides {
|
message DestinationConfigOverrides {
|
||||||
|
@ -36,8 +36,11 @@ message DestinationConfigOverrides {
|
||||||
// This has to be pbcatalog.Service type.
|
// This has to be pbcatalog.Service type.
|
||||||
hashicorp.consul.resource.Reference destination_ref = 1;
|
hashicorp.consul.resource.Reference destination_ref = 1;
|
||||||
|
|
||||||
// DestinationPort is the port name of the destination service. This should be the name
|
// DestinationPort is the port of the destination service.
|
||||||
// of the service's target port. If not provided, this configuration will apply to all ports of an destination.
|
//
|
||||||
|
// For more details on potential values of this field, see documentation for Service.ServicePort.
|
||||||
|
//
|
||||||
|
// If not provided, this configuration will apply to all ports of an destination.
|
||||||
string destination_port = 2;
|
string destination_port = 2;
|
||||||
|
|
||||||
// Datacenter is the datacenter for where this destination service lives.
|
// Datacenter is the datacenter for where this destination service lives.
|
||||||
|
|
|
@ -124,6 +124,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) NewConfig(t *testing.
|
||||||
Enterprise: utils.IsEnterprise(),
|
Enterprise: utils.IsEnterprise(),
|
||||||
Name: clusterName,
|
Name: clusterName,
|
||||||
Nodes: servers,
|
Nodes: servers,
|
||||||
|
Services: make(map[topology.ID]*pbcatalog.Service),
|
||||||
}
|
}
|
||||||
|
|
||||||
lastNode := 0
|
lastNode := 0
|
||||||
|
@ -181,6 +182,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
newID("static-server-v1", tenancy),
|
newID("static-server-v1", tenancy),
|
||||||
topology.NodeVersionV2,
|
topology.NodeVersionV2,
|
||||||
func(wrk *topology.Workload) {
|
func(wrk *topology.Workload) {
|
||||||
|
wrk.V2Services = []string{"static-server-v1", "static-server"}
|
||||||
wrk.Meta = map[string]string{
|
wrk.Meta = map[string]string{
|
||||||
"version": "v1",
|
"version": "v1",
|
||||||
}
|
}
|
||||||
|
@ -200,6 +202,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
newID("static-server-v2", tenancy),
|
newID("static-server-v2", tenancy),
|
||||||
topology.NodeVersionV2,
|
topology.NodeVersionV2,
|
||||||
func(wrk *topology.Workload) {
|
func(wrk *topology.Workload) {
|
||||||
|
wrk.V2Services = []string{"static-server-v2", "static-server"}
|
||||||
wrk.Meta = map[string]string{
|
wrk.Meta = map[string]string{
|
||||||
"version": "v2",
|
"version": "v2",
|
||||||
}
|
}
|
||||||
|
@ -219,6 +222,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
newID("static-client", tenancy),
|
newID("static-client", tenancy),
|
||||||
topology.NodeVersionV2,
|
topology.NodeVersionV2,
|
||||||
func(wrk *topology.Workload) {
|
func(wrk *topology.Workload) {
|
||||||
|
wrk.V2Services = []string{"static-client"}
|
||||||
for i, tenancy := range c.tenancies {
|
for i, tenancy := range c.tenancies {
|
||||||
wrk.Destinations = append(wrk.Destinations, &topology.Destination{
|
wrk.Destinations = append(wrk.Destinations, &topology.Destination{
|
||||||
|
|
||||||
|
@ -296,40 +300,58 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
|
|
||||||
staticServerService := sprawltest.MustSetResourceData(t, &pbresource.Resource{
|
portsFunc := func(offset uint32) []*pbcatalog.ServicePort {
|
||||||
Id: &pbresource.ID{
|
return []*pbcatalog.ServicePort{
|
||||||
Type: pbcatalog.ServiceType,
|
|
||||||
Name: "static-server",
|
|
||||||
Tenancy: tenancy,
|
|
||||||
},
|
|
||||||
}, &pbcatalog.Service{
|
|
||||||
Workloads: &pbcatalog.WorkloadSelector{
|
|
||||||
// This will result in a 50/50 uncontrolled split.
|
|
||||||
Prefixes: []string{"static-server-"},
|
|
||||||
},
|
|
||||||
Ports: []*pbcatalog.ServicePort{
|
|
||||||
{
|
{
|
||||||
TargetPort: "http",
|
TargetPort: "http",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
VirtualPort: 8005 + offset,
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "http2",
|
TargetPort: "http2",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2,
|
VirtualPort: 8006 + offset,
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "grpc",
|
TargetPort: "grpc",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_GRPC,
|
VirtualPort: 9005 + offset,
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_GRPC,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "tcp",
|
TargetPort: "tcp",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
|
VirtualPort: 10005 + offset,
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TargetPort: "mesh",
|
TargetPort: "mesh",
|
||||||
Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
|
Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Differ parent and backend virtual ports to verify we route to each correctly.
|
||||||
|
parentServicePorts := portsFunc(0)
|
||||||
|
backendServicePorts := portsFunc(100)
|
||||||
|
|
||||||
|
// Explicitly define backend services s.t. they are not inferred from workload,
|
||||||
|
// which would assign random virtual ports.
|
||||||
|
cluster.Services[newID("static-client", tenancy)] = &pbcatalog.Service{
|
||||||
|
Ports: []*pbcatalog.ServicePort{
|
||||||
|
{
|
||||||
|
TargetPort: "mesh",
|
||||||
|
Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
cluster.Services[newID("static-server", tenancy)] = &pbcatalog.Service{
|
||||||
|
Ports: parentServicePorts,
|
||||||
|
}
|
||||||
|
cluster.Services[newID("static-server-v1", tenancy)] = &pbcatalog.Service{
|
||||||
|
Ports: backendServicePorts,
|
||||||
|
}
|
||||||
|
cluster.Services[newID("static-server-v2", tenancy)] = &pbcatalog.Service{
|
||||||
|
Ports: backendServicePorts,
|
||||||
|
}
|
||||||
|
|
||||||
httpServerRoute := sprawltest.MustSetResourceData(t, &pbresource.Resource{
|
httpServerRoute := sprawltest.MustSetResourceData(t, &pbresource.Resource{
|
||||||
Id: &pbresource.ID{
|
Id: &pbresource.ID{
|
||||||
|
@ -345,7 +367,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
Name: "static-server",
|
Name: "static-server",
|
||||||
Tenancy: tenancy,
|
Tenancy: tenancy,
|
||||||
},
|
},
|
||||||
Port: "http",
|
Port: "8005", // use mix of target and virtual parent ports
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Ref: &pbresource.Reference{
|
Ref: &pbresource.Reference{
|
||||||
|
@ -406,6 +428,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
Name: "static-server-v1",
|
Name: "static-server-v1",
|
||||||
Tenancy: tenancy,
|
Tenancy: tenancy,
|
||||||
},
|
},
|
||||||
|
Port: "9105", // use mix of virtual and target (inferred from parent) ports
|
||||||
},
|
},
|
||||||
Weight: 10,
|
Weight: 10,
|
||||||
},
|
},
|
||||||
|
@ -436,7 +459,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
Name: "static-server",
|
Name: "static-server",
|
||||||
Tenancy: tenancy,
|
Tenancy: tenancy,
|
||||||
},
|
},
|
||||||
Port: "tcp",
|
Port: "10005", // use virtual parent port
|
||||||
}},
|
}},
|
||||||
Rules: []*pbmesh.TCPRouteRule{{
|
Rules: []*pbmesh.TCPRouteRule{{
|
||||||
BackendRefs: []*pbmesh.TCPBackendRef{
|
BackendRefs: []*pbmesh.TCPBackendRef{
|
||||||
|
@ -447,6 +470,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
Name: "static-server-v1",
|
Name: "static-server-v1",
|
||||||
Tenancy: tenancy,
|
Tenancy: tenancy,
|
||||||
},
|
},
|
||||||
|
Port: "10105", // use explicit virtual port
|
||||||
},
|
},
|
||||||
Weight: 10,
|
Weight: 10,
|
||||||
},
|
},
|
||||||
|
@ -457,6 +481,7 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
Name: "static-server-v2",
|
Name: "static-server-v2",
|
||||||
Tenancy: tenancy,
|
Tenancy: tenancy,
|
||||||
},
|
},
|
||||||
|
Port: "tcp", // use explicit target port
|
||||||
},
|
},
|
||||||
Weight: 90,
|
Weight: 90,
|
||||||
},
|
},
|
||||||
|
@ -471,7 +496,6 @@ func (c testSplitterFeaturesL7ExplicitDestinationsCreator) topologyConfigAddNode
|
||||||
)
|
)
|
||||||
|
|
||||||
cluster.InitialResources = append(cluster.InitialResources,
|
cluster.InitialResources = append(cluster.InitialResources,
|
||||||
staticServerService,
|
|
||||||
v1TrafficPerms,
|
v1TrafficPerms,
|
||||||
v2TrafficPerms,
|
v2TrafficPerms,
|
||||||
httpServerRoute,
|
httpServerRoute,
|
||||||
|
|
|
@ -529,7 +529,7 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.EnableV2 {
|
if c.EnableV2 {
|
||||||
// Populate the VirtualPort field on all implied destinations.
|
// Populate the VirtualPort field on all destinations.
|
||||||
for _, n := range c.Nodes {
|
for _, n := range c.Nodes {
|
||||||
for _, wrk := range n.Workloads {
|
for _, wrk := range n.Workloads {
|
||||||
for _, dest := range wrk.ImpliedDestinations {
|
for _, dest := range wrk.ImpliedDestinations {
|
||||||
|
@ -539,7 +539,20 @@ func compile(logger hclog.Logger, raw *Config, prev *Topology) (*Topology, error
|
||||||
if sp.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
if sp.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if sp.TargetPort == dest.PortName {
|
if sp.MatchesPortId(dest.PortName) {
|
||||||
|
dest.VirtualPort = sp.VirtualPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, dest := range wrk.Destinations {
|
||||||
|
res, ok := c.Services[dest.ID]
|
||||||
|
if ok {
|
||||||
|
for _, sp := range res.Ports {
|
||||||
|
if sp.Protocol == pbcatalog.Protocol_PROTOCOL_MESH {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sp.MatchesPortId(dest.PortName) {
|
||||||
dest.VirtualPort = sp.VirtualPort
|
dest.VirtualPort = sp.VirtualPort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1061,7 +1061,10 @@ type Destination struct {
|
||||||
LocalPort int
|
LocalPort int
|
||||||
Peer string `json:",omitempty"`
|
Peer string `json:",omitempty"`
|
||||||
|
|
||||||
// PortName is the named port of this Destination to route traffic to.
|
// PortName is the port of this Destination to route traffic to.
|
||||||
|
//
|
||||||
|
// For more details on potential values of this field, see documentation
|
||||||
|
// for Service.ServicePort.
|
||||||
//
|
//
|
||||||
// This only applies for multi-port (v2).
|
// This only applies for multi-port (v2).
|
||||||
PortName string `json:",omitempty"`
|
PortName string `json:",omitempty"`
|
||||||
|
|
Loading…
Reference in New Issue