blob: c17b4161abec016b4ca33f553c2d3b39c8f61d0a [file] [log] [blame]
// Copyright Istio Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1alpha3
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
)
import (
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
http "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
meshconfig "istio.io/api/mesh/v1alpha1"
networking "istio.io/api/networking/v1alpha3"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util"
authn_model "github.com/apache/dubbo-go-pixiu/pilot/pkg/security/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/serviceregistry/provider"
xdsfilters "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/filters"
"github.com/apache/dubbo-go-pixiu/pilot/test/xdstest"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
"github.com/apache/dubbo-go-pixiu/pkg/config/labels"
"github.com/apache/dubbo-go-pixiu/pkg/config/protocol"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/assert"
)
func TestApplyDestinationRule(t *testing.T) {
servicePort := model.PortList{
&model.Port{
Name: "default",
Port: 8080,
Protocol: protocol.HTTP,
},
&model.Port{
Name: "auto",
Port: 9090,
Protocol: protocol.Unsupported,
},
}
service := &model.Service{
Hostname: host.Name("foo.default.svc.cluster.local"),
Ports: servicePort,
Resolution: model.ClientSideLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
},
}
cases := []struct {
name string
cluster *cluster.Cluster
clusterMode ClusterMode
service *model.Service
port *model.Port
proxyView model.ProxyView
destRule *networking.DestinationRule
expectedSubsetClusters []*cluster.Cluster
}{
// TODO(ramaraochavali): Add more tests to cover additional conditions.
{
name: "nil destination rule",
cluster: &cluster.Cluster{},
clusterMode: DefaultClusterMode,
service: &model.Service{},
port: &model.Port{},
proxyView: model.ProxyViewAll,
destRule: nil,
expectedSubsetClusters: []*cluster.Cluster{},
},
{
name: "destination rule with subsets",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
Subsets: []*networking.Subset{
{
Name: "foobar",
Labels: map[string]string{"foo": "bar"},
},
},
},
expectedSubsetClusters: []*cluster.Cluster{
{
Name: "outbound|8080|foobar|foo.default.svc.cluster.local",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
ServiceName: "outbound|8080|foobar|foo.default.svc.cluster.local",
},
},
},
},
{
name: "destination rule with pass through subsets",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STATIC}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
Subsets: []*networking.Subset{
{
Name: "foobar",
Labels: map[string]string{"foo": "bar"},
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{Simple: networking.LoadBalancerSettings_PASSTHROUGH},
},
},
},
},
},
expectedSubsetClusters: []*cluster.Cluster{
{
Name: "outbound|8080|foobar|foo.default.svc.cluster.local",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_ORIGINAL_DST},
},
},
},
{
name: "destination rule with subsets for SniDnat cluster",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: SniDnatClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
Subsets: []*networking.Subset{
{
Name: "foobar",
Labels: map[string]string{"foo": "bar"},
},
},
},
expectedSubsetClusters: []*cluster.Cluster{
{
Name: "outbound_.8080_.foobar_.foo.default.svc.cluster.local",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
ServiceName: "outbound_.8080_.foobar_.foo.default.svc.cluster.local",
},
},
},
},
{
name: "destination rule with subset traffic policy",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
Subsets: []*networking.Subset{
{
Name: "foobar",
Labels: map[string]string{"foo": "bar"},
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
},
},
},
expectedSubsetClusters: []*cluster.Cluster{
{
Name: "outbound|8080|foobar|foo.default.svc.cluster.local",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
ServiceName: "outbound|8080|foobar|foo.default.svc.cluster.local",
},
CircuitBreakers: &cluster.CircuitBreakers{
Thresholds: []*cluster.CircuitBreakers_Thresholds{
{
MaxRetries: &wrappers.UInt32Value{
Value: 10,
},
},
},
},
},
},
},
{
name: "destination rule with use client protocol traffic policy",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
UseClientProtocol: true,
},
},
},
},
expectedSubsetClusters: []*cluster.Cluster{},
},
{
name: "destination rule with maxRequestsPerConnection",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
MaxRequestsPerConnection: 10,
},
},
},
},
expectedSubsetClusters: []*cluster.Cluster{},
},
{
name: "subset without labels in both",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{Name: "v1"}},
},
expectedSubsetClusters: []*cluster.Cluster{{
Name: "outbound|8080|v1|foo.example.com",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS},
}},
},
{
name: "subset without labels in dest rule",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
Labels: map[string]string{"foo": "bar"},
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{Name: "v1"}},
},
expectedSubsetClusters: []*cluster.Cluster{{
Name: "outbound|8080|v1|foo.example.com",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS},
}},
},
{
name: "subset with labels in both",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
Labels: map[string]string{"foo": "bar"},
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{
Name: "v1",
Labels: map[string]string{"foo": "bar"},
}},
},
expectedSubsetClusters: []*cluster.Cluster{{
Name: "outbound|8080|v1|foo.example.com",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS},
}},
},
{
name: "subset with labels in both, not matching",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
Labels: map[string]string{"foo": "bar"},
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{
Name: "v1",
Labels: map[string]string{"foo": "not-match"},
}},
},
expectedSubsetClusters: []*cluster.Cluster{},
},
{
name: "subset without labels in both and resolution of DNS_ROUND_ROBIN",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSRoundRobinLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{Name: "v1"}},
},
expectedSubsetClusters: []*cluster.Cluster{{
Name: "outbound|8080|v1|foo.example.com",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS},
}},
},
{
name: "subset without labels in dest rule and a resolution of DNS_ROUND_ROBIN",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSRoundRobinLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
Labels: map[string]string{"foo": "bar"},
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{Name: "v1"}},
},
expectedSubsetClusters: []*cluster.Cluster{{
Name: "outbound|8080|v1|foo.example.com",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS},
}},
},
{
name: "subset with labels in both",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSRoundRobinLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
Labels: map[string]string{"foo": "bar"},
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{
Name: "v1",
Labels: map[string]string{"foo": "bar"},
}},
},
expectedSubsetClusters: []*cluster.Cluster{{
Name: "outbound|8080|v1|foo.example.com",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS},
}},
},
{
name: "subset with labels in both, not matching",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_LOGICAL_DNS}},
clusterMode: DefaultClusterMode,
service: &model.Service{
Hostname: host.Name("foo.example.com"),
Ports: servicePort,
Resolution: model.DNSRoundRobinLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
Labels: map[string]string{"foo": "bar"},
},
},
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.example.com",
Subsets: []*networking.Subset{{
Name: "v1",
Labels: map[string]string{"foo": "not-match"},
}},
},
expectedSubsetClusters: []*cluster.Cluster{},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
instances := []*model.ServiceInstance{
{
Service: tt.service,
ServicePort: tt.port,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.1",
EndpointPort: 10001,
Locality: model.Locality{
ClusterID: "",
Label: "region1/zone1/subzone1",
},
Labels: tt.service.Attributes.Labels,
TLSMode: model.IstioMutualTLSModeLabel,
},
},
}
var cfg *config.Config
if tt.destRule != nil {
cfg = &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.DestinationRule,
Name: "acme",
Namespace: "default",
},
Spec: tt.destRule,
}
}
cg := NewConfigGenTest(t, TestOptions{
Instances: instances,
ConfigPointers: []*config.Config{cfg},
Services: []*model.Service{tt.service},
})
cg.MemRegistry.WantGetProxyServiceInstances = instances
proxy := cg.SetupProxy(nil)
cb := NewClusterBuilder(proxy, &model.PushRequest{Push: cg.PushContext()}, nil)
ec := NewMutableCluster(tt.cluster)
destRule := proxy.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, proxy, tt.service.Hostname)
subsetClusters := cb.applyDestinationRule(ec, tt.clusterMode, tt.service, tt.port, tt.proxyView, destRule, nil)
if len(subsetClusters) != len(tt.expectedSubsetClusters) {
t.Fatalf("Unexpected subset clusters want %v, got %v. keys=%v",
len(tt.expectedSubsetClusters), len(subsetClusters), xdstest.MapKeys(xdstest.ExtractClusters(subsetClusters)))
}
if len(tt.expectedSubsetClusters) > 0 {
compareClusters(t, tt.expectedSubsetClusters[0], subsetClusters[0])
}
// Validate that use client protocol configures cluster correctly.
if tt.destRule != nil && tt.destRule.TrafficPolicy != nil && tt.destRule.TrafficPolicy.GetConnectionPool().GetHttp().UseClientProtocol {
if ec.httpProtocolOptions == nil {
t.Errorf("Expected cluster %s to have http protocol options but not found", tt.cluster.Name)
}
if ec.httpProtocolOptions.UpstreamProtocolOptions == nil &&
ec.httpProtocolOptions.GetUseDownstreamProtocolConfig() == nil {
t.Errorf("Expected cluster %s to have downstream protocol options but not found", tt.cluster.Name)
}
}
// Validate that use client protocol configures cluster correctly.
if tt.destRule != nil && tt.destRule.TrafficPolicy != nil && tt.destRule.TrafficPolicy.GetConnectionPool().GetHttp().MaxRequestsPerConnection > 0 {
if ec.httpProtocolOptions == nil {
t.Errorf("Expected cluster %s to have http protocol options but not found", tt.cluster.Name)
}
if ec.httpProtocolOptions.CommonHttpProtocolOptions == nil {
t.Errorf("Expected cluster %s to have common http protocol options but not found", tt.cluster.Name)
}
if ec.httpProtocolOptions.CommonHttpProtocolOptions.MaxRequestsPerConnection.GetValue() !=
uint32(tt.destRule.TrafficPolicy.GetConnectionPool().GetHttp().MaxRequestsPerConnection) {
t.Errorf("Unexpected max_requests_per_connection found")
}
}
// Validate that ORIGINAL_DST cluster does not have load assignments
for _, subset := range subsetClusters {
if subset.GetType() == cluster.Cluster_ORIGINAL_DST && subset.GetLoadAssignment() != nil {
t.Errorf("Passthrough subsets should not have load assignments")
}
}
})
}
}
func compareClusters(t *testing.T, ec *cluster.Cluster, gc *cluster.Cluster) {
// TODO(ramaraochavali): Expand the comparison to more fields.
t.Helper()
if ec.Name != gc.Name {
t.Errorf("Unexpected cluster name want %s, got %s", ec.Name, gc.Name)
}
if ec.GetType() != gc.GetType() {
t.Errorf("Unexpected cluster discovery type want %v, got %v", ec.GetType(), gc.GetType())
}
if ec.GetType() == cluster.Cluster_EDS && ec.EdsClusterConfig.ServiceName != gc.EdsClusterConfig.ServiceName {
t.Errorf("Unexpected service name in EDS config want %v, got %v", ec.EdsClusterConfig.ServiceName, gc.EdsClusterConfig.ServiceName)
}
if ec.CircuitBreakers != nil {
if ec.CircuitBreakers.Thresholds[0].MaxRetries.Value != gc.CircuitBreakers.Thresholds[0].MaxRetries.Value {
t.Errorf("Unexpected circuit breaker thresholds want %v, got %v", ec.CircuitBreakers.Thresholds[0].MaxRetries, gc.CircuitBreakers.Thresholds[0].MaxRetries)
}
}
}
func TestMergeTrafficPolicy(t *testing.T) {
cases := []struct {
name string
original *networking.TrafficPolicy
subset *networking.TrafficPolicy
port *model.Port
expected *networking.TrafficPolicy
}{
{
name: "all nil policies",
original: nil,
subset: nil,
port: nil,
expected: nil,
},
{
name: "no subset policy",
original: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
subset: nil,
port: nil,
expected: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
},
{
name: "no parent policy",
original: nil,
subset: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
port: nil,
expected: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
},
{
name: "merge non-conflicting fields",
original: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
},
},
subset: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
port: nil,
expected: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
},
},
},
{
name: "subset overwrite top-level fields",
original: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
},
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
subset: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
port: nil,
expected: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
},
},
{
name: "merge port level policy, and do not inherit top-level fields",
original: nil,
subset: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
Port: &networking.PortSelector{
Number: 8080,
},
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_LEAST_REQUEST,
},
},
},
},
},
port: &model.Port{Port: 8080},
expected: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_LEAST_REQUEST,
},
},
},
},
{
name: "merge port level policy, and do not inherit top-level fields",
original: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 20,
},
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
Port: &networking.PortSelector{
Number: 8080,
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 15,
},
},
},
},
subset: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
Port: &networking.PortSelector{
Number: 8080,
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 13,
},
},
},
},
port: &model.Port{Port: 8080},
expected: &networking.TrafficPolicy{
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 13,
},
},
},
{
name: "default cluster, non-matching port selector",
original: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 20,
},
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
Port: &networking.PortSelector{
Number: 8080,
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 15,
},
},
},
},
subset: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
Port: &networking.PortSelector{
Number: 8080,
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 13,
},
},
},
},
port: &model.Port{Port: 9090},
expected: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
},
},
OutlierDetection: &networking.OutlierDetection{
ConsecutiveErrors: 20,
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
policy := MergeTrafficPolicy(tt.original, tt.subset, tt.port)
if !reflect.DeepEqual(policy, tt.expected) {
t.Errorf("Unexpected merged TrafficPolicy. want %v, got %v", tt.expected, policy)
}
})
}
}
func TestApplyEdsConfig(t *testing.T) {
cases := []struct {
name string
cluster *cluster.Cluster
edsConfig *cluster.Cluster_EdsClusterConfig
}{
{
name: "non eds type of cluster",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STRICT_DNS}},
edsConfig: nil,
},
{
name: "eds type of cluster",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
edsConfig: &cluster.Cluster_EdsClusterConfig{
ServiceName: "foo",
EdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_Ads{
Ads: &core.AggregatedConfigSource{},
},
InitialFetchTimeout: durationpb.New(0),
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
maybeApplyEdsConfig(tt.cluster)
if !reflect.DeepEqual(tt.cluster.EdsClusterConfig, tt.edsConfig) {
t.Errorf("Unexpected Eds config in cluster. want %v, got %v", tt.edsConfig, tt.cluster.EdsClusterConfig)
}
})
}
}
func TestBuildDefaultCluster(t *testing.T) {
servicePort := &model.Port{
Name: "default",
Port: 8080,
Protocol: protocol.HTTP,
}
cases := []struct {
name string
clusterName string
discovery cluster.Cluster_DiscoveryType
endpoints []*endpoint.LocalityLbEndpoints
direction model.TrafficDirection
external bool
expectedCluster *cluster.Cluster
}{
{
name: "default EDS cluster",
clusterName: "foo",
discovery: cluster.Cluster_EDS,
endpoints: nil,
direction: model.TrafficDirectionOutbound,
external: false,
expectedCluster: &cluster.Cluster{
Name: "foo",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
ConnectTimeout: &durationpb.Duration{Seconds: 10, Nanos: 1},
CircuitBreakers: &cluster.CircuitBreakers{
Thresholds: []*cluster.CircuitBreakers_Thresholds{getDefaultCircuitBreakerThresholds()},
},
Filters: []*cluster.Filter{xdsfilters.TCPClusterMx},
LbPolicy: defaultLBAlgorithm(),
Metadata: &core.Metadata{
FilterMetadata: map[string]*structpb.Struct{
util.IstioMetadataKey: {
Fields: map[string]*structpb.Value{
"services": {Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{
{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: map[string]*structpb.Value{
"host": {
Kind: &structpb.Value_StringValue{
StringValue: "host",
},
},
"name": {
Kind: &structpb.Value_StringValue{
StringValue: "svc",
},
},
"namespace": {
Kind: &structpb.Value_StringValue{
StringValue: "default",
},
},
}}}},
}}}},
"default_original_port": {
Kind: &structpb.Value_NumberValue{
NumberValue: float64(8080),
},
},
},
},
},
},
EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
ServiceName: "foo",
EdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_Ads{
Ads: &core.AggregatedConfigSource{},
},
InitialFetchTimeout: durationpb.New(0),
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
{
name: "static cluster with no endpoints",
clusterName: "foo",
discovery: cluster.Cluster_STATIC,
endpoints: nil,
direction: model.TrafficDirectionOutbound,
external: false,
expectedCluster: nil,
},
{
name: "strict DNS cluster with no endpoints",
clusterName: "foo",
discovery: cluster.Cluster_STRICT_DNS,
endpoints: nil,
direction: model.TrafficDirectionOutbound,
external: false,
expectedCluster: nil,
},
{
name: "static cluster with endpoints",
clusterName: "foo",
discovery: cluster.Cluster_STATIC,
endpoints: []*endpoint.LocalityLbEndpoints{
{
Locality: &core.Locality{
Region: "region1",
Zone: "zone1",
SubZone: "subzone1",
},
LbEndpoints: []*endpoint.LbEndpoint{},
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 1,
},
Priority: 0,
},
},
direction: model.TrafficDirectionOutbound,
external: false,
expectedCluster: &cluster.Cluster{
Name: "foo",
ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_STATIC},
ConnectTimeout: &durationpb.Duration{Seconds: 10, Nanos: 1},
Filters: []*cluster.Filter{xdsfilters.TCPClusterMx},
LbPolicy: defaultLBAlgorithm(),
LoadAssignment: &endpoint.ClusterLoadAssignment{
ClusterName: "foo",
Endpoints: []*endpoint.LocalityLbEndpoints{
{
Locality: &core.Locality{
Region: "region1",
Zone: "zone1",
SubZone: "subzone1",
},
LbEndpoints: []*endpoint.LbEndpoint{},
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 1,
},
Priority: 0,
},
},
},
CircuitBreakers: &cluster.CircuitBreakers{
Thresholds: []*cluster.CircuitBreakers_Thresholds{getDefaultCircuitBreakerThresholds()},
},
Metadata: &core.Metadata{
FilterMetadata: map[string]*structpb.Struct{
util.IstioMetadataKey: {Fields: map[string]*structpb.Value{
"services": {Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{
{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: map[string]*structpb.Value{
"host": {
Kind: &structpb.Value_StringValue{
StringValue: "host",
},
},
"name": {
Kind: &structpb.Value_StringValue{
StringValue: "svc",
},
},
"namespace": {
Kind: &structpb.Value_StringValue{
StringValue: "default",
},
},
}}}},
}}}},
"default_original_port": {
Kind: &structpb.Value_NumberValue{
NumberValue: float64(8080),
},
},
}},
},
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
mesh := testMesh()
cg := NewConfigGenTest(t, TestOptions{MeshConfig: mesh})
cb := NewClusterBuilder(cg.SetupProxy(nil), &model.PushRequest{Push: cg.PushContext()}, nil)
service := &model.Service{
Ports: model.PortList{
servicePort,
},
Hostname: "host",
MeshExternal: false,
Attributes: model.ServiceAttributes{Name: "svc", Namespace: "default"},
}
defaultCluster := cb.buildDefaultCluster(tt.clusterName, tt.discovery, tt.endpoints, tt.direction, servicePort, service, nil)
if defaultCluster != nil {
_ = cb.applyDestinationRule(defaultCluster, DefaultClusterMode, service, servicePort, cb.proxyView, nil, nil)
}
if diff := cmp.Diff(defaultCluster.build(), tt.expectedCluster, protocmp.Transform()); diff != "" {
t.Errorf("Unexpected default cluster, diff: %v", diff)
}
})
}
}
func TestBuildLocalityLbEndpoints(t *testing.T) {
proxy := &model.Proxy{
Metadata: &model.NodeMetadata{
ClusterID: "cluster-1",
},
}
servicePort := &model.Port{
Name: "default",
Port: 8080,
Protocol: protocol.HTTP,
}
service := &model.Service{
Hostname: host.Name("*.example.org"),
Ports: model.PortList{servicePort},
Attributes: model.ServiceAttributes{
Name: "TestService",
Namespace: "test-ns",
},
}
cases := []struct {
name string
mesh *meshconfig.MeshConfig
labels labels.Instance
instances []*model.ServiceInstance
expected []*endpoint.LocalityLbEndpoints
}{
{
name: "basics",
mesh: testMesh(),
instances: []*model.ServiceInstance{
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.1",
EndpointPort: 10001,
WorkloadName: "workload-1",
Namespace: "namespace-1",
Locality: model.Locality{
ClusterID: "cluster-1",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
Network: "nw-0",
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.2",
EndpointPort: 10001,
WorkloadName: "workload-2",
Namespace: "namespace-2",
Locality: model.Locality{
ClusterID: "cluster-2",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
Network: "nw-1",
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.3",
EndpointPort: 10001,
WorkloadName: "workload-3",
Namespace: "namespace-3",
Locality: model.Locality{
ClusterID: "cluster-3",
Label: "region2/zone1/subzone1",
},
LbWeight: 40,
Network: "",
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.4",
EndpointPort: 10001,
WorkloadName: "workload-1",
Namespace: "namespace-1",
Locality: model.Locality{
ClusterID: "cluster-1",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
Network: "filtered-out",
},
},
},
expected: []*endpoint.LocalityLbEndpoints{
{
Locality: &core.Locality{
Region: "region1",
Zone: "zone1",
SubZone: "subzone1",
},
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 60,
},
LbEndpoints: []*endpoint.LbEndpoint{
{
HostIdentifier: &endpoint.LbEndpoint_Endpoint{
Endpoint: &endpoint.Endpoint{
Address: &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: "192.168.1.1",
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: 10001,
},
},
},
},
},
},
Metadata: util.BuildLbEndpointMetadata("nw-0", "", "workload-1", "namespace-1", "cluster-1", map[string]string{}),
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 30,
},
},
{
HostIdentifier: &endpoint.LbEndpoint_Endpoint{
Endpoint: &endpoint.Endpoint{
Address: &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: "192.168.1.2",
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: 10001,
},
},
},
},
},
},
Metadata: util.BuildLbEndpointMetadata("nw-1", "", "workload-2", "namespace-2", "cluster-2", map[string]string{}),
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 30,
},
},
},
},
{
Locality: &core.Locality{
Region: "region2",
Zone: "zone1",
SubZone: "subzone1",
},
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 40,
},
LbEndpoints: []*endpoint.LbEndpoint{
{
HostIdentifier: &endpoint.LbEndpoint_Endpoint{
Endpoint: &endpoint.Endpoint{
Address: &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: "192.168.1.3",
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: 10001,
},
},
},
},
},
},
Metadata: util.BuildLbEndpointMetadata("", "", "workload-3", "namespace-3", "cluster-3", map[string]string{}),
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 40,
},
},
},
},
},
},
{
name: "cluster local",
mesh: withClusterLocalHosts(testMesh(), "*.example.org"),
instances: []*model.ServiceInstance{
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.1",
EndpointPort: 10001,
Locality: model.Locality{
ClusterID: "cluster-1",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.2",
EndpointPort: 10001,
Locality: model.Locality{
ClusterID: "cluster-2",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
},
},
},
expected: []*endpoint.LocalityLbEndpoints{
{
Locality: &core.Locality{
Region: "region1",
Zone: "zone1",
SubZone: "subzone1",
},
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 30,
},
LbEndpoints: []*endpoint.LbEndpoint{
{
HostIdentifier: &endpoint.LbEndpoint_Endpoint{
Endpoint: &endpoint.Endpoint{
Address: &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: "192.168.1.1",
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: 10001,
},
},
},
},
},
},
Metadata: util.BuildLbEndpointMetadata("", "", "", "", "cluster-1", map[string]string{}),
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 30,
},
},
},
},
},
},
{
name: "subset cluster endpoints with labels",
mesh: testMesh(),
labels: labels.Instance{"version": "v1"},
instances: []*model.ServiceInstance{
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.1",
EndpointPort: 10001,
WorkloadName: "workload-1",
Namespace: "namespace-1",
Labels: map[string]string{
"version": "v1",
"app": "example",
},
Locality: model.Locality{
ClusterID: "cluster-1",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
Network: "nw-0",
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.2",
EndpointPort: 10001,
WorkloadName: "workload-2",
Namespace: "namespace-2",
Labels: map[string]string{
"version": "v2",
"app": "example",
},
Locality: model.Locality{
ClusterID: "cluster-2",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
Network: "nw-1",
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.3",
EndpointPort: 10001,
WorkloadName: "workload-3",
Namespace: "namespace-3",
Labels: map[string]string{
"version": "v3",
"app": "example",
},
Locality: model.Locality{
ClusterID: "cluster-3",
Label: "region2/zone1/subzone1",
},
LbWeight: 40,
Network: "",
},
},
{
Service: service,
ServicePort: servicePort,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.4",
EndpointPort: 10001,
WorkloadName: "workload-1",
Namespace: "namespace-1",
Labels: map[string]string{
"version": "v4",
"app": "example",
},
Locality: model.Locality{
ClusterID: "cluster-1",
Label: "region1/zone1/subzone1",
},
LbWeight: 30,
Network: "filtered-out",
},
},
},
expected: []*endpoint.LocalityLbEndpoints{
{
Locality: &core.Locality{
Region: "region1",
Zone: "zone1",
SubZone: "subzone1",
},
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 30,
},
LbEndpoints: []*endpoint.LbEndpoint{
{
HostIdentifier: &endpoint.LbEndpoint_Endpoint{
Endpoint: &endpoint.Endpoint{
Address: &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: "192.168.1.1",
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: 10001,
},
},
},
},
},
},
Metadata: util.BuildLbEndpointMetadata("nw-0", "", "workload-1", "namespace-1", "cluster-1", map[string]string{}),
LoadBalancingWeight: &wrappers.UInt32Value{
Value: 30,
},
},
},
},
},
},
}
sortEndpoints := func(endpoints []*endpoint.LocalityLbEndpoints) {
sort.SliceStable(endpoints, func(i, j int) bool {
if strings.Compare(endpoints[i].Locality.Region, endpoints[j].Locality.Region) < 0 {
return true
}
if strings.Compare(endpoints[i].Locality.Zone, endpoints[j].Locality.Zone) < 0 {
return true
}
return strings.Compare(endpoints[i].Locality.SubZone, endpoints[j].Locality.SubZone) < 0
})
}
for _, tt := range cases {
for _, resolution := range []model.Resolution{model.DNSLB, model.DNSRoundRobinLB} {
t.Run(fmt.Sprintf("%s_%s", tt.name, resolution), func(t *testing.T) {
service.Resolution = resolution
cg := NewConfigGenTest(t, TestOptions{
MeshConfig: tt.mesh,
Services: []*model.Service{service},
Instances: tt.instances,
})
cb := NewClusterBuilder(cg.SetupProxy(proxy), &model.PushRequest{Push: cg.PushContext()}, nil)
view := (&model.Proxy{
Metadata: &model.NodeMetadata{
RequestedNetworkView: []string{"nw-0", "nw-1"},
},
}).GetView()
actual := cb.buildLocalityLbEndpoints(view, service, 8080, tt.labels)
sortEndpoints(actual)
if v := cmp.Diff(tt.expected, actual, protocmp.Transform()); v != "" {
t.Fatalf("Expected (-) != actual (+):\n%s", v)
}
})
}
}
}
func TestBuildPassthroughClusters(t *testing.T) {
cases := []struct {
name string
ips []string
ipv4Expected bool
ipv6Expected bool
}{
{
name: "both ipv4 and ipv6",
ips: []string{"6.6.6.6", "::1"},
ipv4Expected: true,
ipv6Expected: true,
},
{
name: "ipv4 only",
ips: []string{"6.6.6.6"},
ipv4Expected: true,
ipv6Expected: false,
},
{
name: "ipv6 only",
ips: []string{"::1"},
ipv4Expected: false,
ipv6Expected: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
proxy := &model.Proxy{IPAddresses: tt.ips}
cg := NewConfigGenTest(t, TestOptions{})
cb := NewClusterBuilder(cg.SetupProxy(proxy), &model.PushRequest{Push: cg.PushContext()}, nil)
clusters := cb.buildInboundPassthroughClusters()
var hasIpv4, hasIpv6 bool
for _, c := range clusters {
hasIpv4 = hasIpv4 || c.Name == util.InboundPassthroughClusterIpv4
hasIpv6 = hasIpv6 || c.Name == util.InboundPassthroughClusterIpv6
}
if hasIpv4 != tt.ipv4Expected {
t.Errorf("Unexpected Ipv4 Passthrough Cluster, want %v got %v", tt.ipv4Expected, hasIpv4)
}
if hasIpv6 != tt.ipv6Expected {
t.Errorf("Unexpected Ipv6 Passthrough Cluster, want %v got %v", tt.ipv6Expected, hasIpv6)
}
passthrough := xdstest.ExtractCluster(util.InboundPassthroughClusterIpv4, clusters)
if passthrough == nil {
passthrough = xdstest.ExtractCluster(util.InboundPassthroughClusterIpv6, clusters)
}
// Validate that Passthrough Cluster LB Policy is set correctly.
if passthrough.GetType() != cluster.Cluster_ORIGINAL_DST || passthrough.GetLbPolicy() != cluster.Cluster_CLUSTER_PROVIDED {
t.Errorf("Unexpected Discovery type or Lb policy, got Discovery type: %v, Lb Policy: %v", passthrough.GetType(), passthrough.GetLbPolicy())
}
})
}
}
func TestApplyUpstreamTLSSettings(t *testing.T) {
istioMutualTLSSettings := &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
SubjectAltNames: []string{"custom.foo.com"},
Sni: "custom.foo.com",
}
mutualTLSSettingsWithCerts := &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CaCertificates: constants.DefaultRootCert,
ClientCertificate: constants.DefaultCertChain,
PrivateKey: constants.DefaultKey,
SubjectAltNames: []string{"custom.foo.com"},
Sni: "custom.foo.com",
}
simpleTLSSettingsWithCerts := &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CaCertificates: constants.DefaultRootCert,
SubjectAltNames: []string{"custom.foo.com"},
Sni: "custom.foo.com",
}
tests := []struct {
name string
mtlsCtx mtlsContextType
discoveryType cluster.Cluster_DiscoveryType
tls *networking.ClientTLSSettings
h2 bool
expectTransportSocket bool
expectTransportSocketMatch bool
validateTLSContext func(t *testing.T, ctx *tls.UpstreamTlsContext)
}{
{
name: "user specified without tls",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: nil,
expectTransportSocket: false,
expectTransportSocketMatch: false,
},
{
name: "user specified with istio_mutual tls",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: istioMutualTLSSettings,
expectTransportSocket: true,
expectTransportSocketMatch: false,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
if got := ctx.CommonTlsContext.GetAlpnProtocols(); !reflect.DeepEqual(got, util.ALPNInMeshWithMxc) {
t.Fatalf("expected alpn list %v; got %v", util.ALPNInMeshWithMxc, got)
}
},
},
{
name: "user specified with istio_mutual tls with h2",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: istioMutualTLSSettings,
expectTransportSocket: true,
expectTransportSocketMatch: false,
h2: true,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
if got := ctx.CommonTlsContext.GetAlpnProtocols(); !reflect.DeepEqual(got, util.ALPNInMeshH2WithMxc) {
t.Fatalf("expected alpn list %v; got %v", util.ALPNInMeshH2WithMxc, got)
}
},
},
{
name: "user specified simple tls",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: simpleTLSSettingsWithCerts,
expectTransportSocket: true,
expectTransportSocketMatch: false,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
rootName := "file-root:" + mutualTLSSettingsWithCerts.CaCertificates
if got := ctx.CommonTlsContext.GetCombinedValidationContext().GetValidationContextSdsSecretConfig().GetName(); rootName != got {
t.Fatalf("expected root name %v got %v", rootName, got)
}
if got := ctx.CommonTlsContext.GetAlpnProtocols(); got != nil {
t.Fatalf("expected alpn list nil as not h2 or Istio_Mutual TLS Setting; got %v", got)
}
if got := ctx.GetSni(); got != simpleTLSSettingsWithCerts.Sni {
t.Fatalf("expected TLSContext SNI %v; got %v", simpleTLSSettingsWithCerts.Sni, got)
}
},
},
{
name: "user specified simple tls with h2",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: simpleTLSSettingsWithCerts,
expectTransportSocket: true,
expectTransportSocketMatch: false,
h2: true,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
rootName := "file-root:" + mutualTLSSettingsWithCerts.CaCertificates
if got := ctx.CommonTlsContext.GetCombinedValidationContext().GetValidationContextSdsSecretConfig().GetName(); rootName != got {
t.Fatalf("expected root name %v got %v", rootName, got)
}
if got := ctx.CommonTlsContext.GetAlpnProtocols(); !reflect.DeepEqual(got, util.ALPNH2Only) {
t.Fatalf("expected alpn list %v; got %v", util.ALPNH2Only, got)
}
if got := ctx.GetSni(); got != simpleTLSSettingsWithCerts.Sni {
t.Fatalf("expected TLSContext SNI %v; got %v", simpleTLSSettingsWithCerts.Sni, got)
}
},
},
{
name: "user specified mutual tls",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: mutualTLSSettingsWithCerts,
expectTransportSocket: true,
expectTransportSocketMatch: false,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
rootName := "file-root:" + mutualTLSSettingsWithCerts.CaCertificates
certName := fmt.Sprintf("file-cert:%s~%s", mutualTLSSettingsWithCerts.ClientCertificate, mutualTLSSettingsWithCerts.PrivateKey)
if got := ctx.CommonTlsContext.GetCombinedValidationContext().GetValidationContextSdsSecretConfig().GetName(); rootName != got {
t.Fatalf("expected root name %v got %v", rootName, got)
}
if got := ctx.CommonTlsContext.GetTlsCertificateSdsSecretConfigs()[0].GetName(); certName != got {
t.Fatalf("expected cert name %v got %v", certName, got)
}
if got := ctx.CommonTlsContext.GetAlpnProtocols(); got != nil {
t.Fatalf("expected alpn list nil as not h2 or Istio_Mutual TLS Setting; got %v", got)
}
if got := ctx.GetSni(); got != mutualTLSSettingsWithCerts.Sni {
t.Fatalf("expected TLSContext SNI %v; got %v", mutualTLSSettingsWithCerts.Sni, got)
}
},
},
{
name: "user specified mutual tls with h2",
mtlsCtx: userSupplied,
discoveryType: cluster.Cluster_EDS,
tls: mutualTLSSettingsWithCerts,
expectTransportSocket: true,
expectTransportSocketMatch: false,
h2: true,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
rootName := "file-root:" + mutualTLSSettingsWithCerts.CaCertificates
certName := fmt.Sprintf("file-cert:%s~%s", mutualTLSSettingsWithCerts.ClientCertificate, mutualTLSSettingsWithCerts.PrivateKey)
if got := ctx.CommonTlsContext.GetCombinedValidationContext().GetValidationContextSdsSecretConfig().GetName(); rootName != got {
t.Fatalf("expected root name %v got %v", rootName, got)
}
if got := ctx.CommonTlsContext.GetTlsCertificateSdsSecretConfigs()[0].GetName(); certName != got {
t.Fatalf("expected cert name %v got %v", certName, got)
}
if got := ctx.CommonTlsContext.GetAlpnProtocols(); !reflect.DeepEqual(got, util.ALPNH2Only) {
t.Fatalf("expected alpn list %v; got %v", util.ALPNH2Only, got)
}
if got := ctx.GetSni(); got != mutualTLSSettingsWithCerts.Sni {
t.Fatalf("expected TLSContext SNI %v; got %v", mutualTLSSettingsWithCerts.Sni, got)
}
},
},
{
name: "auto detect with tls",
mtlsCtx: autoDetected,
discoveryType: cluster.Cluster_EDS,
tls: istioMutualTLSSettings,
expectTransportSocket: false,
expectTransportSocketMatch: true,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
if got := ctx.CommonTlsContext.GetAlpnProtocols(); !reflect.DeepEqual(got, util.ALPNInMeshWithMxc) {
t.Fatalf("expected alpn list %v; got %v", util.ALPNInMeshWithMxc, got)
}
},
},
{
name: "auto detect with tls and h2 options",
mtlsCtx: autoDetected,
discoveryType: cluster.Cluster_EDS,
tls: istioMutualTLSSettings,
expectTransportSocket: false,
expectTransportSocketMatch: true,
h2: true,
validateTLSContext: func(t *testing.T, ctx *tls.UpstreamTlsContext) {
if got := ctx.CommonTlsContext.GetAlpnProtocols(); !reflect.DeepEqual(got, util.ALPNInMeshH2WithMxc) {
t.Fatalf("expected alpn list %v; got %v", util.ALPNInMeshH2WithMxc, got)
}
},
},
}
proxy := &model.Proxy{
Type: model.SidecarProxy,
Metadata: &model.NodeMetadata{},
IstioVersion: &model.IstioVersion{Major: 1, Minor: 5},
}
push := model.NewPushContext()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cb := NewClusterBuilder(proxy, &model.PushRequest{Push: push}, model.DisabledCache{})
opts := &buildClusterOpts{
mutable: NewMutableCluster(&cluster.Cluster{
ClusterDiscoveryType: &cluster.Cluster_Type{Type: test.discoveryType},
}),
mesh: push.Mesh,
}
if test.h2 {
cb.setH2Options(opts.mutable)
}
cb.applyUpstreamTLSSettings(opts, test.tls, test.mtlsCtx)
if test.expectTransportSocket && opts.mutable.cluster.TransportSocket == nil ||
!test.expectTransportSocket && opts.mutable.cluster.TransportSocket != nil {
t.Errorf("Expected TransportSocket %v", test.expectTransportSocket)
}
if test.expectTransportSocketMatch && opts.mutable.cluster.TransportSocketMatches == nil ||
!test.expectTransportSocketMatch && opts.mutable.cluster.TransportSocketMatches != nil {
t.Errorf("Expected TransportSocketMatch %v", test.expectTransportSocketMatch)
}
if test.validateTLSContext != nil {
ctx := &tls.UpstreamTlsContext{}
if test.expectTransportSocket {
if err := opts.mutable.cluster.TransportSocket.GetTypedConfig().UnmarshalTo(ctx); err != nil {
t.Fatal(err)
}
} else if test.expectTransportSocketMatch {
if err := opts.mutable.cluster.TransportSocketMatches[0].TransportSocket.GetTypedConfig().UnmarshalTo(ctx); err != nil {
t.Fatal(err)
}
}
test.validateTLSContext(t, ctx)
}
})
}
}
type expectedResult struct {
tlsContext *tls.UpstreamTlsContext
err error
}
// TestBuildUpstreamClusterTLSContext tests the buildUpstreamClusterTLSContext function
func TestBuildUpstreamClusterTLSContext(t *testing.T) {
clientCert := "/path/to/cert"
rootCert := "path/to/cacert"
clientKey := "/path/to/key"
credentialName := "some-fake-credential"
testCases := []struct {
name string
opts *buildClusterOpts
tls *networking.ClientTLSSettings
h2 bool
router bool
result expectedResult
enableAutoSni bool
enableVerifyCertAtClient bool
}{
{
name: "tls mode disabled",
opts: &buildClusterOpts{
mutable: NewMutableCluster(&cluster.Cluster{
Name: "test-cluster",
}),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_DISABLE,
},
result: expectedResult{nil, nil},
},
{
name: "tls mode ISTIO_MUTUAL",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: "default",
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
InitialFetchTimeout: durationpb.New(time.Second * 0),
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "ROOTCA",
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
InitialFetchTimeout: durationpb.New(time.Second * 0),
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
AlpnProtocols: util.ALPNInMeshWithMxc,
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode ISTIO_MUTUAL and H2",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
h2: true,
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: "default",
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
InitialFetchTimeout: durationpb.New(time.Second * 0),
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "ROOTCA",
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
InitialFetchTimeout: durationpb.New(time.Second * 0),
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
AlpnProtocols: util.ALPNInMeshH2WithMxc,
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with no certs specified in tls",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_ValidationContext{},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with AutoSni enabled and no sni specified in tls",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
SubjectAltNames: []string{"SAN"},
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_ValidationContext{},
},
},
err: nil,
},
enableAutoSni: true,
},
{
name: "tls mode SIMPLE, with AutoSni enabled and sni specified in tls",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_ValidationContext{},
},
Sni: "some-sni.com",
},
err: nil,
},
enableAutoSni: true,
},
{
name: "tls mode SIMPLE, with VerifyCert and AutoSni enabled with SubjectAltNames set",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_ValidationContext{},
},
Sni: "some-sni.com",
},
err: nil,
},
enableAutoSni: true,
enableVerifyCertAtClient: true,
},
{
name: "tls mode SIMPLE, with VerifyCert and AutoSni enabled without SubjectAltNames set",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_ValidationContext{},
},
Sni: "some-sni.com",
},
err: nil,
},
enableAutoSni: true,
enableVerifyCertAtClient: true,
},
{
name: "tls mode SIMPLE, with certs specified in tls",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CaCertificates: rootCert,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: fmt.Sprintf("file-root:%s", rootCert),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with certs specified in tls with h2",
opts: &buildClusterOpts{
mutable: newH2TestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CaCertificates: rootCert,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
h2: true,
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: fmt.Sprintf("file-root:%s", rootCert),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
AlpnProtocols: util.ALPNH2Only,
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with certs specified in tls",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CaCertificates: rootCert,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: fmt.Sprintf("file-root:%s", rootCert),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with SANs specified in service entries",
opts: &buildClusterOpts{
mutable: newTestCluster(),
serviceAccounts: []string{"se-san.com"},
serviceRegistry: provider.External,
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CaCertificates: rootCert,
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"se-san.com"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: fmt.Sprintf("file-root:%s", rootCert),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode MUTUAL, with no client certificate",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: "",
PrivateKey: "some-fake-key",
},
result: expectedResult{
nil,
fmt.Errorf("client cert must be provided"),
},
},
{
name: "tls mode MUTUAL, with no client key",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: "some-fake-cert",
PrivateKey: "",
},
result: expectedResult{
nil,
fmt.Errorf("client key must be provided"),
},
},
{
name: "tls mode MUTUAL, with node metadata sdsEnabled true no root CA specified",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: clientCert,
PrivateKey: clientKey,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: fmt.Sprintf("file-cert:%s~%s", clientCert, clientKey),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
ValidationContextType: &tls.CommonTlsContext_ValidationContext{},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode MUTUAL, with node metadata sdsEnabled true",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: clientCert,
PrivateKey: clientKey,
CaCertificates: rootCert,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: fmt.Sprintf("file-cert:%s~%s", clientCert, clientKey),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"})},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: fmt.Sprintf("file-root:%s", rootCert),
SdsConfig: &core.ConfigSource{
ConfigSourceSpecifier: &core.ConfigSource_ApiConfigSource{
ApiConfigSource: &core.ApiConfigSource{
ApiType: core.ApiConfigSource_GRPC,
SetNodeOnFirstMessageOnly: true,
TransportApiVersion: core.ApiVersion_V3,
GrpcServices: []*core.GrpcService{
{
TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "sds-grpc"},
},
},
},
},
},
ResourceApiVersion: core.ApiVersion_V3,
},
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with CredentialName specified",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: credentialName,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
router: true,
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{
MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"}),
},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "kubernetes://" + credentialName + authn_model.SdsCaSuffix,
SdsConfig: authn_model.SDSAdsConfig,
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode SIMPLE, with CredentialName specified with h2 and no SAN",
opts: &buildClusterOpts{
mutable: newH2TestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: credentialName,
Sni: "some-sni.com",
},
h2: true,
router: true,
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "kubernetes://" + credentialName + authn_model.SdsCaSuffix,
SdsConfig: authn_model.SDSAdsConfig,
},
},
},
AlpnProtocols: util.ALPNH2Only,
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode MUTUAL, with CredentialName specified",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: credentialName,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
router: true,
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: "kubernetes://" + credentialName,
SdsConfig: authn_model.SDSAdsConfig,
},
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{
MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"}),
},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "kubernetes://" + credentialName + authn_model.SdsCaSuffix,
SdsConfig: authn_model.SDSAdsConfig,
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode MUTUAL, with CredentialName specified with h2 and no SAN",
opts: &buildClusterOpts{
mutable: newH2TestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: credentialName,
Sni: "some-sni.com",
},
h2: true,
router: true,
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: "kubernetes://" + credentialName,
SdsConfig: authn_model.SDSAdsConfig,
},
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "kubernetes://" + credentialName + authn_model.SdsCaSuffix,
SdsConfig: authn_model.SDSAdsConfig,
},
},
},
AlpnProtocols: util.ALPNH2Only,
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode MUTUAL, credentialName is set with proxy type Sidecar",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: "fake-cred",
},
result: expectedResult{
nil,
nil,
},
},
{
name: "tls mode SIMPLE, credentialName is set with proxy type Sidecar",
opts: &buildClusterOpts{
mutable: newTestCluster(),
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: "fake-cred",
},
result: expectedResult{
nil,
nil,
},
},
{
name: "tls mode SIMPLE, CredentialName is set with proxy type Sidecar and destinationRule has workload Selector",
opts: &buildClusterOpts{
mutable: newTestCluster(),
isDrWithSelector: true,
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: credentialName,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{
MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"}),
},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "kubernetes://" + credentialName + authn_model.SdsCaSuffix,
SdsConfig: authn_model.SDSAdsConfig,
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
{
name: "tls mode MUTUAL, CredentialName is set with proxy type Sidecar and destinationRule has workload Selector",
opts: &buildClusterOpts{
mutable: newTestCluster(),
isDrWithSelector: true,
},
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: credentialName,
SubjectAltNames: []string{"SAN"},
Sni: "some-sni.com",
},
result: expectedResult{
tlsContext: &tls.UpstreamTlsContext{
CommonTlsContext: &tls.CommonTlsContext{
TlsParams: &tls.TlsParameters{
// if not specified, envoy use TLSv1_2 as default for client.
TlsMaximumProtocolVersion: tls.TlsParameters_TLSv1_3,
TlsMinimumProtocolVersion: tls.TlsParameters_TLSv1_2,
},
TlsCertificateSdsSecretConfigs: []*tls.SdsSecretConfig{
{
Name: "kubernetes://" + credentialName,
SdsConfig: authn_model.SDSAdsConfig,
},
},
ValidationContextType: &tls.CommonTlsContext_CombinedValidationContext{
CombinedValidationContext: &tls.CommonTlsContext_CombinedCertificateValidationContext{
DefaultValidationContext: &tls.CertificateValidationContext{
MatchSubjectAltNames: util.StringToExactMatch([]string{"SAN"}),
},
ValidationContextSdsSecretConfig: &tls.SdsSecretConfig{
Name: "kubernetes://" + credentialName + authn_model.SdsCaSuffix,
SdsConfig: authn_model.SDSAdsConfig,
},
},
},
},
Sni: "some-sni.com",
},
err: nil,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
test.SetBoolForTest(t, &features.EnableAutoSni, tc.enableAutoSni)
test.SetBoolForTest(t, &features.VerifyCertAtClient, tc.enableVerifyCertAtClient)
var proxy *model.Proxy
if tc.router {
proxy = newGatewayProxy()
} else {
proxy = newSidecarProxy()
}
cb := NewClusterBuilder(proxy, nil, model.DisabledCache{})
if tc.h2 {
cb.setH2Options(tc.opts.mutable)
}
ret, err := cb.buildUpstreamClusterTLSContext(tc.opts, tc.tls)
if err != nil && tc.result.err == nil || err == nil && tc.result.err != nil {
t.Errorf("expecting:\n err=%v but got err=%v", tc.result.err, err)
} else if diff := cmp.Diff(tc.result.tlsContext, ret, protocmp.Transform()); diff != "" {
t.Errorf("got diff: `%v", diff)
}
if tc.enableAutoSni {
if len(tc.tls.Sni) == 0 {
assert.Equal(t, tc.opts.mutable.httpProtocolOptions.UpstreamHttpProtocolOptions.AutoSni, true)
}
if tc.enableVerifyCertAtClient && len(tc.tls.SubjectAltNames) == 0 {
assert.Equal(t, tc.opts.mutable.httpProtocolOptions.UpstreamHttpProtocolOptions.AutoSanValidation, true)
}
}
})
}
}
func newTestCluster() *MutableCluster {
return NewMutableCluster(&cluster.Cluster{
Name: "test-cluster",
})
}
func newH2TestCluster() *MutableCluster {
cb := NewClusterBuilder(newSidecarProxy(), nil, model.DisabledCache{})
mc := NewMutableCluster(&cluster.Cluster{
Name: "test-cluster",
})
cb.setH2Options(mc)
return mc
}
func newDownstreamTestCluster() *MutableCluster {
cb := NewClusterBuilder(newSidecarProxy(), nil, model.DisabledCache{})
mc := NewMutableCluster(&cluster.Cluster{
Name: "test-cluster",
})
cb.setUseDownstreamProtocol(mc)
return mc
}
func newSidecarProxy() *model.Proxy {
return &model.Proxy{Type: model.SidecarProxy, Metadata: &model.NodeMetadata{}}
}
func newGatewayProxy() *model.Proxy {
return &model.Proxy{Type: model.Router, Metadata: &model.NodeMetadata{}}
}
// Helper function to extract TLS context from a cluster
func getTLSContext(t *testing.T, c *cluster.Cluster) *tls.UpstreamTlsContext {
t.Helper()
if c.TransportSocket == nil {
return nil
}
tlsContext := &tls.UpstreamTlsContext{}
err := c.TransportSocket.GetTypedConfig().UnmarshalTo(tlsContext)
if err != nil {
t.Fatalf("Failed to unmarshall tls context: %v", err)
}
return tlsContext
}
func TestShouldH2Upgrade(t *testing.T) {
tests := []struct {
name string
clusterName string
direction model.TrafficDirection
port *model.Port
mesh *meshconfig.MeshConfig
connectionPool *networking.ConnectionPoolSettings
upgrade bool
}{
{
name: "mesh upgrade - dr default",
clusterName: "bar",
direction: model.TrafficDirectionOutbound,
port: &model.Port{Protocol: protocol.HTTP},
mesh: &meshconfig.MeshConfig{H2UpgradePolicy: meshconfig.MeshConfig_UPGRADE},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_DEFAULT,
},
},
upgrade: true,
},
{
name: "mesh default - dr upgrade non http port",
clusterName: "bar",
direction: model.TrafficDirectionOutbound,
port: &model.Port{Protocol: protocol.Unsupported},
mesh: &meshconfig.MeshConfig{},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_UPGRADE,
},
},
upgrade: true,
},
{
name: "mesh no_upgrade - dr default",
clusterName: "bar",
direction: model.TrafficDirectionOutbound,
port: &model.Port{Protocol: protocol.HTTP},
mesh: &meshconfig.MeshConfig{H2UpgradePolicy: meshconfig.MeshConfig_DO_NOT_UPGRADE},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_DEFAULT,
},
},
upgrade: false,
},
{
name: "mesh no_upgrade - dr upgrade",
clusterName: "bar",
direction: model.TrafficDirectionOutbound,
port: &model.Port{Protocol: protocol.HTTP},
mesh: &meshconfig.MeshConfig{H2UpgradePolicy: meshconfig.MeshConfig_DO_NOT_UPGRADE},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_UPGRADE,
},
},
upgrade: true,
},
{
name: "mesh upgrade - dr no_upgrade",
clusterName: "bar",
direction: model.TrafficDirectionOutbound,
port: &model.Port{Protocol: protocol.HTTP},
mesh: &meshconfig.MeshConfig{H2UpgradePolicy: meshconfig.MeshConfig_UPGRADE},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_DO_NOT_UPGRADE,
},
},
upgrade: false,
},
{
name: "inbound ignore",
clusterName: "bar",
direction: model.TrafficDirectionInbound,
port: &model.Port{Protocol: protocol.HTTP},
mesh: &meshconfig.MeshConfig{H2UpgradePolicy: meshconfig.MeshConfig_UPGRADE},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_DEFAULT,
},
},
upgrade: false,
},
{
name: "non-http",
clusterName: "bar",
direction: model.TrafficDirectionOutbound,
port: &model.Port{Protocol: protocol.Unsupported},
mesh: &meshconfig.MeshConfig{H2UpgradePolicy: meshconfig.MeshConfig_UPGRADE},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_DEFAULT,
},
},
upgrade: false,
},
}
cb := NewClusterBuilder(newSidecarProxy(), nil, model.DisabledCache{})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
upgrade := cb.shouldH2Upgrade(test.clusterName, test.direction, test.port, test.mesh, test.connectionPool)
if upgrade != test.upgrade {
t.Fatalf("got: %t, want: %t (%v, %v)", upgrade, test.upgrade, test.mesh.H2UpgradePolicy, test.connectionPool.Http.H2UpgradePolicy)
}
})
}
}
// nolint
func TestIsHttp2Cluster(t *testing.T) {
tests := []struct {
name string
cluster *MutableCluster
isHttp2Cluster bool
}{
{
name: "with no h2 options",
cluster: newTestCluster(),
isHttp2Cluster: false,
},
{
name: "with h2 options",
cluster: newH2TestCluster(),
isHttp2Cluster: true,
},
{
name: "with downstream config and h2 options",
cluster: newDownstreamTestCluster(),
isHttp2Cluster: false,
},
}
cb := NewClusterBuilder(newSidecarProxy(), nil, model.DisabledCache{})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
isHttp2Cluster := cb.IsHttp2Cluster(test.cluster)
if isHttp2Cluster != test.isHttp2Cluster {
t.Errorf("got: %t, want: %t", isHttp2Cluster, test.isHttp2Cluster)
}
})
}
}
func TestBuildAutoMtlsSettings(t *testing.T) {
tlsSettings := &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
SubjectAltNames: []string{"custom.foo.com"},
Sni: "custom.foo.com",
}
tests := []struct {
name string
tls *networking.ClientTLSSettings
sans []string
sni string
proxy *model.Proxy
autoMTLSEnabled bool
meshExternal bool
serviceMTLSMode model.MutualTLSMode
want *networking.ClientTLSSettings
wantCtxType mtlsContextType
}{
{
"Destination rule TLS sni and SAN override",
tlsSettings,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
false, false, model.MTLSUnknown,
tlsSettings,
userSupplied,
},
{
"Metadata cert path override ISTIO_MUTUAL",
tlsSettings,
[]string{"custom.foo.com"},
"custom.foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{
TLSClientCertChain: "/custom/chain.pem",
TLSClientKey: "/custom/key.pem",
TLSClientRootCert: "/custom/root.pem",
}},
false, false, model.MTLSUnknown,
&networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
PrivateKey: "/custom/key.pem",
ClientCertificate: "/custom/chain.pem",
CaCertificates: "/custom/root.pem",
SubjectAltNames: []string{"custom.foo.com"},
Sni: "custom.foo.com",
},
userSupplied,
},
{
"Auto fill nil settings when mTLS nil for internal service in strict mode",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
true, false, model.MTLSStrict,
&networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
SubjectAltNames: []string{"spiffe://foo/serviceaccount/1"},
Sni: "foo.com",
},
autoDetected,
},
{
"Auto fill nil settings when mTLS nil for internal service in permissive mode",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
true, false, model.MTLSPermissive,
&networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
SubjectAltNames: []string{"spiffe://foo/serviceaccount/1"},
Sni: "foo.com",
},
autoDetected,
},
{
"Auto fill nil settings when mTLS nil for internal service in plaintext mode",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
true, false, model.MTLSDisable,
nil,
userSupplied,
},
{
"Auto fill nil settings when mTLS nil for internal service in unknown mode",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
true, false, model.MTLSUnknown,
nil,
userSupplied,
},
{
"Do not auto fill nil settings for external",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
true, true, model.MTLSUnknown,
nil,
userSupplied,
},
{
"Do not auto fill nil settings if server mTLS is disabled",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{}},
false, false, model.MTLSDisable,
nil,
userSupplied,
},
{
"TLS nil auto build tls with metadata cert path",
nil,
[]string{"spiffe://foo/serviceaccount/1"},
"foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{
TLSClientCertChain: "/custom/chain.pem",
TLSClientKey: "/custom/key.pem",
TLSClientRootCert: "/custom/root.pem",
}},
true, false, model.MTLSPermissive,
&networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: "/custom/chain.pem",
PrivateKey: "/custom/key.pem",
CaCertificates: "/custom/root.pem",
SubjectAltNames: []string{"spiffe://foo/serviceaccount/1"},
Sni: "foo.com",
},
autoDetected,
},
{
"Simple TLS",
&networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
PrivateKey: "/custom/key.pem",
ClientCertificate: "/custom/chain.pem",
CaCertificates: "/custom/root.pem",
},
[]string{"custom.foo.com"},
"custom.foo.com",
&model.Proxy{Metadata: &model.NodeMetadata{
TLSClientCertChain: "/custom/meta/chain.pem",
TLSClientKey: "/custom/meta/key.pem",
TLSClientRootCert: "/custom/meta/root.pem",
}},
false, false, model.MTLSUnknown,
&networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
PrivateKey: "/custom/key.pem",
ClientCertificate: "/custom/chain.pem",
CaCertificates: "/custom/root.pem",
},
userSupplied,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cb := NewClusterBuilder(tt.proxy, nil, nil)
gotTLS, gotCtxType := cb.buildAutoMtlsSettings(tt.tls, tt.sans, tt.sni,
tt.autoMTLSEnabled, tt.meshExternal, tt.serviceMTLSMode)
if !reflect.DeepEqual(gotTLS, tt.want) {
t.Errorf("cluster TLS does not match expected result want %#v, got %#v", tt.want, gotTLS)
}
if gotCtxType != tt.wantCtxType {
t.Errorf("cluster TLS context type does not match expected result want %#v, got %#v", tt.wantCtxType, gotCtxType)
}
})
}
}
func TestApplyDestinationRuleOSCACert(t *testing.T) {
servicePort := model.PortList{
&model.Port{
Name: "default",
Port: 8080,
Protocol: protocol.HTTP,
},
&model.Port{
Name: "auto",
Port: 9090,
Protocol: protocol.Unsupported,
},
}
service := &model.Service{
Hostname: host.Name("foo.default.svc.cluster.local"),
Ports: servicePort,
Resolution: model.ClientSideLB,
Attributes: model.ServiceAttributes{
Namespace: TestServiceNamespace,
},
}
cases := []struct {
name string
cluster *cluster.Cluster
clusterMode ClusterMode
service *model.Service
port *model.Port
proxyView model.ProxyView
destRule *networking.DestinationRule
expectedCaCertificateName string
enableVerifyCertAtClient bool
}{
{
name: "VerifyCertAtClient set and destination rule with empty string CaCertificates",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
UseClientProtocol: true,
},
},
Tls: &networking.ClientTLSSettings{
CaCertificates: "",
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
},
expectedCaCertificateName: "system",
enableVerifyCertAtClient: true,
},
{
name: "VerifyCertAtClient set and destination rule with CaCertificates",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
UseClientProtocol: true,
},
},
Tls: &networking.ClientTLSSettings{
CaCertificates: constants.DefaultRootCert,
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
},
expectedCaCertificateName: constants.DefaultRootCert,
enableVerifyCertAtClient: true,
},
{
name: "VerifyCertAtClient set and destination rule without CaCertificates",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
UseClientProtocol: true,
},
},
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
},
expectedCaCertificateName: "system",
enableVerifyCertAtClient: true,
},
{
name: "VerifyCertAtClient false and destination rule without CaCertificates",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
UseClientProtocol: true,
},
},
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
},
expectedCaCertificateName: "",
enableVerifyCertAtClient: false,
},
{
name: "VerifyCertAtClient false and destination rule with CaCertificates",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
clusterMode: DefaultClusterMode,
service: service,
port: servicePort[0],
proxyView: model.ProxyViewAll,
destRule: &networking.DestinationRule{
Host: "foo.default.svc.cluster.local",
TrafficPolicy: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRetries: 10,
UseClientProtocol: true,
},
},
Tls: &networking.ClientTLSSettings{
CaCertificates: constants.DefaultRootCert,
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
},
expectedCaCertificateName: constants.DefaultRootCert,
enableVerifyCertAtClient: false,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
test.SetBoolForTest(t, &features.VerifyCertAtClient, tt.enableVerifyCertAtClient)
instances := []*model.ServiceInstance{
{
Service: tt.service,
ServicePort: tt.port,
Endpoint: &model.IstioEndpoint{
Address: "192.168.1.1",
EndpointPort: 10001,
Locality: model.Locality{
ClusterID: "",
Label: "region1/zone1/subzone1",
},
TLSMode: model.IstioMutualTLSModeLabel,
},
},
}
var cfg *config.Config
if tt.destRule != nil {
cfg = &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.DestinationRule,
Name: "acme",
Namespace: "default",
},
Spec: tt.destRule,
}
}
cg := NewConfigGenTest(t, TestOptions{
ConfigPointers: []*config.Config{cfg},
Services: []*model.Service{tt.service},
})
cg.MemRegistry.WantGetProxyServiceInstances = instances
proxy := cg.SetupProxy(nil)
cb := NewClusterBuilder(proxy, &model.PushRequest{Push: cg.PushContext()}, nil)
ec := NewMutableCluster(tt.cluster)
destRule := proxy.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, proxy, tt.service.Hostname)
// ACT
_ = cb.applyDestinationRule(ec, tt.clusterMode, tt.service, tt.port, tt.proxyView, destRule, nil)
byteArray, err := config.ToJSON(destRule.Spec)
if err != nil {
t.Errorf("Could not parse destination rule: %v", err)
}
dr := &networking.DestinationRule{}
err = json.Unmarshal(byteArray, dr)
if err != nil {
t.Errorf("Could not unmarshal destination rule: %v", err)
}
ca := dr.TrafficPolicy.Tls.CaCertificates
if ca != tt.expectedCaCertificateName {
t.Errorf("%v: got unexpected caCertitifcates field. Expected (%v), received (%v)", tt.name, tt.expectedCaCertificateName, ca)
}
})
}
}
func TestApplyTCPKeepalive(t *testing.T) {
cases := []struct {
name string
mesh *meshconfig.MeshConfig
connectionPool *networking.ConnectionPoolSettings
wantConnOpts *cluster.UpstreamConnectionOptions
}{
{
name: "no tcp alive",
mesh: &meshconfig.MeshConfig{},
connectionPool: &networking.ConnectionPoolSettings{},
wantConnOpts: nil,
},
{
name: "destination rule tcp alive",
mesh: &meshconfig.MeshConfig{},
connectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{
TcpKeepalive: &networking.ConnectionPoolSettings_TCPSettings_TcpKeepalive{
Time: &durationpb.Duration{Seconds: 10},
},
},
},
wantConnOpts: &cluster.UpstreamConnectionOptions{
TcpKeepalive: &core.TcpKeepalive{
KeepaliveTime: &wrappers.UInt32Value{Value: uint32(10)},
},
},
},
{
name: "mesh tcp alive",
mesh: &meshconfig.MeshConfig{
TcpKeepalive: &networking.ConnectionPoolSettings_TCPSettings_TcpKeepalive{
Time: &durationpb.Duration{Seconds: 10},
},
},
connectionPool: &networking.ConnectionPoolSettings{},
wantConnOpts: &cluster.UpstreamConnectionOptions{
TcpKeepalive: &core.TcpKeepalive{
KeepaliveTime: &wrappers.UInt32Value{Value: uint32(10)},
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
cg := NewConfigGenTest(t, TestOptions{})
proxy := cg.SetupProxy(nil)
cb := NewClusterBuilder(proxy, &model.PushRequest{Push: cg.PushContext()}, nil)
mc := &MutableCluster{
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
}
cb.applyConnectionPool(tt.mesh, mc, tt.connectionPool)
if !reflect.DeepEqual(tt.wantConnOpts, mc.cluster.UpstreamConnectionOptions) {
t.Errorf("unexpected tcp keepalive settings, want %v, got %v", tt.wantConnOpts,
mc.cluster.UpstreamConnectionOptions)
}
})
}
}
func TestApplyConnectionPool(t *testing.T) {
// only test connectionPool.Http.IdleTimeout and connectionPool.Http.IdleTimeout.MaxRequestsPerConnection
cases := []struct {
name string
cluster *cluster.Cluster
httpProtocolOptions *http.HttpProtocolOptions
connectionPool *networking.ConnectionPoolSettings
expectedHTTPPOpt *http.HttpProtocolOptions
}{
{
name: "only update IdleTimeout",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
httpProtocolOptions: &http.HttpProtocolOptions{
CommonHttpProtocolOptions: &core.HttpProtocolOptions{
IdleTimeout: &durationpb.Duration{
Seconds: 10,
},
MaxRequestsPerConnection: &wrappers.UInt32Value{Value: 10},
},
},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
IdleTimeout: &durationpb.Duration{
Seconds: 22,
},
},
},
expectedHTTPPOpt: &http.HttpProtocolOptions{
CommonHttpProtocolOptions: &core.HttpProtocolOptions{
IdleTimeout: &durationpb.Duration{
Seconds: 22,
},
MaxRequestsPerConnection: &wrappers.UInt32Value{Value: 10},
},
},
},
{
name: "only update MaxRequestsPerConnection ",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
httpProtocolOptions: &http.HttpProtocolOptions{
CommonHttpProtocolOptions: &core.HttpProtocolOptions{
IdleTimeout: &durationpb.Duration{
Seconds: 10,
},
MaxRequestsPerConnection: &wrappers.UInt32Value{Value: 10},
},
},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
MaxRequestsPerConnection: 22,
},
},
expectedHTTPPOpt: &http.HttpProtocolOptions{
CommonHttpProtocolOptions: &core.HttpProtocolOptions{
IdleTimeout: &durationpb.Duration{
Seconds: 10,
},
MaxRequestsPerConnection: &wrappers.UInt32Value{Value: 22},
},
},
},
{
name: "update MaxRequestsPerConnection and IdleTimeout",
cluster: &cluster.Cluster{Name: "foo", ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS}},
httpProtocolOptions: &http.HttpProtocolOptions{
CommonHttpProtocolOptions: &core.HttpProtocolOptions{
IdleTimeout: &durationpb.Duration{
Seconds: 10,
},
MaxRequestsPerConnection: &wrappers.UInt32Value{Value: 10},
},
},
connectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
IdleTimeout: &durationpb.Duration{
Seconds: 22,
},
MaxRequestsPerConnection: 22,
},
},
expectedHTTPPOpt: &http.HttpProtocolOptions{
CommonHttpProtocolOptions: &core.HttpProtocolOptions{
IdleTimeout: &durationpb.Duration{
Seconds: 22,
},
MaxRequestsPerConnection: &wrappers.UInt32Value{Value: 22},
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
cg := NewConfigGenTest(t, TestOptions{})
proxy := cg.SetupProxy(nil)
cb := NewClusterBuilder(proxy, &model.PushRequest{Push: cg.PushContext()}, nil)
mc := &MutableCluster{
cluster: tt.cluster,
httpProtocolOptions: tt.httpProtocolOptions,
}
opts := buildClusterOpts{
mesh: cb.req.Push.Mesh,
mutable: mc,
}
cb.applyConnectionPool(opts.mesh, opts.mutable, tt.connectionPool)
// assert httpProtocolOptions
assert.Equal(t, opts.mutable.httpProtocolOptions.CommonHttpProtocolOptions.IdleTimeout,
tt.expectedHTTPPOpt.CommonHttpProtocolOptions.IdleTimeout)
assert.Equal(t, opts.mutable.httpProtocolOptions.CommonHttpProtocolOptions.MaxRequestsPerConnection,
tt.expectedHTTPPOpt.CommonHttpProtocolOptions.MaxRequestsPerConnection)
})
}
}