blob: deaa6b2eec75959d236d7f9c89dcc1aa4c303f61 [file] [log] [blame]
// Copyright Istio Authors
//
// 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 validation
import (
"strings"
"testing"
"time"
)
import (
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/hashicorp/go-multierror"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
extensions "istio.io/api/extensions/v1alpha1"
meshconfig "istio.io/api/mesh/v1alpha1"
networking "istio.io/api/networking/v1alpha3"
networkingv1beta1 "istio.io/api/networking/v1beta1"
security_beta "istio.io/api/security/v1beta1"
telemetry "istio.io/api/telemetry/v1alpha1"
api "istio.io/api/type/v1beta1"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/assert"
)
const (
// Config name for testing
someName = "foo"
// Config namespace for testing.
someNamespace = "bar"
)
func TestValidateFQDN(t *testing.T) {
tests := []struct {
fqdn string
valid bool
name string
}{
{
fqdn: strings.Repeat("x", 256),
valid: false,
name: "long FQDN",
},
{
fqdn: "",
valid: false,
name: "empty FQDN",
},
{
fqdn: "istio.io",
valid: true,
name: "standard FQDN",
},
{
fqdn: "istio.io.",
valid: true,
name: "unambiguous FQDN",
},
{
fqdn: "istio-pilot.dubbo-system.svc.cluster.local",
valid: true,
name: "standard kubernetes FQDN",
},
{
fqdn: "istio-pilot.dubbo-system.svc.cluster.local.",
valid: true,
name: "unambiguous kubernetes FQDN",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
err := ValidateFQDN(tt.fqdn)
valid := err == nil
if valid != tt.valid {
t.Errorf("Expected valid=%v, got valid=%v for %v", tt.valid, valid, tt.fqdn)
}
})
}
}
func TestValidateWildcardDomain(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{"empty", "", "empty"},
{"too long", strings.Repeat("x", 256), "too long"},
{"happy", strings.Repeat("x", 63), ""},
{"wildcard", "*", ""},
{"wildcard multi-segment", "*.bar.com", ""},
{"wildcard single segment", "*foo", ""},
{"wildcard prefix", "*foo.bar.com", ""},
{"wildcard prefix dash", "*-foo.bar.com", ""},
{"bad wildcard", "foo.*.com", "invalid"},
{"bad wildcard", "foo*.bar.com", "invalid"},
{"IP address", "1.1.1.1", "invalid"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateWildcardDomain(tt.in)
if err == nil && tt.out != "" {
t.Fatalf("ValidateWildcardDomain(%v) = nil, wanted %q", tt.in, tt.out)
} else if err != nil && tt.out == "" {
t.Fatalf("ValidateWildcardDomain(%v) = %v, wanted nil", tt.in, err)
} else if err != nil && !strings.Contains(err.Error(), tt.out) {
t.Fatalf("ValidateWildcardDomain(%v) = %v, wanted %q", tt.in, err, tt.out)
}
})
}
}
func TestValidateTrustDomain(t *testing.T) {
tests := []struct {
name string
in string
err string
}{
{"empty", "", "empty"},
{"happy", strings.Repeat("x", 63), ""},
{"multi-segment", "foo.bar.com", ""},
{"middle dash", "f-oo.bar.com", ""},
{"trailing dot", "foo.bar.com.", ""},
{"prefix dash", "-foo.bar.com", "invalid"},
{"forward slash separated", "foo/bar/com", "invalid"},
{"colon separated", "foo:bar:com", "invalid"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateTrustDomain(tt.in)
if err == nil && tt.err != "" {
t.Fatalf("ValidateTrustDomain(%v) = nil, wanted %q", tt.in, tt.err)
} else if err != nil && tt.err == "" {
t.Fatalf("ValidateTrustDomain(%v) = %v, wanted nil", tt.in, err)
} else if err != nil && !strings.Contains(err.Error(), tt.err) {
t.Fatalf("ValidateTrustDomain(%v) = %v, wanted %q", tt.in, err, tt.err)
}
})
}
}
func TestValidatePort(t *testing.T) {
ports := map[int]bool{
0: false,
65536: false,
-1: false,
100: true,
1000: true,
65535: true,
}
for port, valid := range ports {
if got := ValidatePort(port); (got == nil) != valid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for %d", got == nil, valid, got, port)
}
}
}
func TestValidateControlPlaneAuthPolicy(t *testing.T) {
cases := []struct {
name string
policy meshconfig.AuthenticationPolicy
isValid bool
}{
{
name: "invalid policy",
policy: -1,
isValid: false,
},
{
name: "valid policy",
policy: 0,
isValid: true,
},
{
name: "valid policy",
policy: 1,
isValid: true,
},
{
name: "invalid policy",
policy: 2,
isValid: false,
},
{
name: "invalid policy",
policy: 100,
isValid: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := ValidateControlPlaneAuthPolicy(c.policy); (got == nil) != c.isValid {
t.Errorf("got valid=%v but wanted valid=%v: %v", got == nil, c.isValid, got)
}
})
}
}
func TestValidateProxyAddress(t *testing.T) {
addresses := map[string]bool{
"istio-pilot:80": true,
"istio-pilot": false,
"isti..:80": false,
"10.0.0.100:9090": true,
"10.0.0.100": false,
"istio-pilot:port": false,
"istio-pilot:100000": false,
"[2001:db8::100]:80": true,
"[2001:db8::10::20]:80": false,
"[2001:db8::100]": false,
"[2001:db8::100]:port": false,
"2001:db8::100:80": false,
}
for addr, valid := range addresses {
if got := ValidateProxyAddress(addr); (got == nil) != valid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for %s", got == nil, valid, got, addr)
}
}
}
func TestValidateDuration(t *testing.T) {
type durationCheck struct {
duration *durationpb.Duration
isValid bool
}
checks := []durationCheck{
{
duration: &durationpb.Duration{Seconds: 1},
isValid: true,
},
{
duration: &durationpb.Duration{Seconds: 1, Nanos: -1},
isValid: false,
},
{
duration: &durationpb.Duration{Seconds: -11, Nanos: -1},
isValid: false,
},
{
duration: &durationpb.Duration{Nanos: 1},
isValid: false,
},
{
duration: &durationpb.Duration{Seconds: 1, Nanos: 1},
isValid: false,
},
}
for _, check := range checks {
if got := ValidateDuration(check.duration); (got == nil) != check.isValid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for %v", got == nil, check.isValid, got, check.duration)
}
}
}
func TestValidateParentAndDrain(t *testing.T) {
type ParentDrainTime struct {
Parent *durationpb.Duration
Drain *durationpb.Duration
Valid bool
}
combinations := []ParentDrainTime{
{
Parent: &durationpb.Duration{Seconds: 2},
Drain: &durationpb.Duration{Seconds: 1},
Valid: true,
},
{
Parent: &durationpb.Duration{Seconds: 1},
Drain: &durationpb.Duration{Seconds: 1},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: 1},
Drain: &durationpb.Duration{Seconds: 2},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: 2},
Drain: &durationpb.Duration{Seconds: 1, Nanos: 1000000},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: 2, Nanos: 1000000},
Drain: &durationpb.Duration{Seconds: 1},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: -2},
Drain: &durationpb.Duration{Seconds: 1},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: 2},
Drain: &durationpb.Duration{Seconds: -1},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: 1 + int64(time.Hour/time.Second)},
Drain: &durationpb.Duration{Seconds: 10},
Valid: false,
},
{
Parent: &durationpb.Duration{Seconds: 10},
Drain: &durationpb.Duration{Seconds: 1 + int64(time.Hour/time.Second)},
Valid: false,
},
}
for _, combo := range combinations {
if got := ValidateParentAndDrain(combo.Drain, combo.Parent); (got == nil) != combo.Valid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for Parent:%v Drain:%v",
got == nil, combo.Valid, got, combo.Parent, combo.Drain)
}
}
}
func TestValidateConnectTimeout(t *testing.T) {
type durationCheck struct {
duration *durationpb.Duration
isValid bool
}
checks := []durationCheck{
{
duration: &durationpb.Duration{Seconds: 1},
isValid: true,
},
{
duration: &durationpb.Duration{Seconds: 31},
isValid: false,
},
{
duration: &durationpb.Duration{Nanos: 99999},
isValid: false,
},
}
for _, check := range checks {
if got := ValidateConnectTimeout(check.duration); (got == nil) != check.isValid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for %v", got == nil, check.isValid, got, check.duration)
}
}
}
func TestValidateMaxServerConnectionAge(t *testing.T) {
type durationCheck struct {
duration time.Duration
isValid bool
}
durMin, _ := time.ParseDuration("-30m")
durHr, _ := time.ParseDuration("-1.5h")
checks := []durationCheck{
{
duration: 30 * time.Minute,
isValid: true,
},
{
duration: durMin,
isValid: false,
},
{
duration: durHr,
isValid: false,
},
}
for _, check := range checks {
if got := ValidateMaxServerConnectionAge(check.duration); (got == nil) != check.isValid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for %v", got == nil, check.isValid, got, check.duration)
}
}
}
func TestValidateProtocolDetectionTimeout(t *testing.T) {
type durationCheck struct {
duration *durationpb.Duration
isValid bool
}
checks := []durationCheck{
{
duration: &durationpb.Duration{Seconds: 1},
isValid: true,
},
{
duration: &durationpb.Duration{Nanos: 99999},
isValid: false,
},
{
duration: &durationpb.Duration{Nanos: 0},
isValid: true,
},
}
for _, check := range checks {
if got := ValidateProtocolDetectionTimeout(check.duration); (got == nil) != check.isValid {
t.Errorf("Failed: got valid=%t but wanted valid=%t: %v for %v", got == nil, check.isValid, got, check.duration)
}
}
}
func TestValidateMeshConfig(t *testing.T) {
if ValidateMeshConfig(&meshconfig.MeshConfig{}) == nil {
t.Error("expected an error on an empty mesh config")
}
invalid := &meshconfig.MeshConfig{
ProxyListenPort: 0,
ConnectTimeout: durationpb.New(-1 * time.Second),
DefaultConfig: &meshconfig.ProxyConfig{},
TrustDomain: "",
TrustDomainAliases: []string{"a.$b", "a/b", ""},
ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
{
Name: "default",
Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp{
EnvoyExtAuthzHttp: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider{
Service: "foo/ext-authz",
Port: 999999,
},
},
},
},
}
err := ValidateMeshConfig(invalid)
if err == nil {
t.Errorf("expected an error on invalid proxy mesh config: %v", invalid)
} else {
wantErrors := []string{
"invalid proxy listen port",
"invalid connect timeout",
"config path must be set",
"binary path must be set",
"oneof service cluster or tracing service name must be specified",
"invalid parent and drain time combination invalid drain duration",
"invalid parent and drain time combination invalid parent shutdown duration",
"discovery address must be set to the proxy discovery service",
"invalid proxy admin port",
"invalid status port",
"trustDomain: empty domain name not allowed",
"trustDomainAliases[0]",
"trustDomainAliases[1]",
"trustDomainAliases[2]",
}
switch err := err.(type) {
case *multierror.Error:
// each field must cause an error in the field
if len(err.Errors) != len(wantErrors) {
t.Errorf("expected %d errors but found %v", len(wantErrors), err)
} else {
for i := 0; i < len(wantErrors); i++ {
if !strings.HasPrefix(err.Errors[i].Error(), wantErrors[i]) {
t.Errorf("expected error %q at index %d but found %q", wantErrors[i], i, err.Errors[i])
}
}
}
default:
t.Errorf("expected a multi error as output")
}
}
}
func TestValidateMeshConfigProxyConfig(t *testing.T) {
valid := &meshconfig.ProxyConfig{
ConfigPath: "/etc/istio/proxy",
BinaryPath: "/usr/local/bin/envoy",
DiscoveryAddress: "istio-pilot.dubbo-system:15010",
ProxyAdminPort: 15000,
DrainDuration: durationpb.New(45 * time.Second),
ParentShutdownDuration: durationpb.New(60 * time.Second),
ClusterName: &meshconfig.ProxyConfig_ServiceCluster{ServiceCluster: "istio-proxy"},
StatsdUdpAddress: "istio-statsd-prom-bridge.dubbo-system:9125",
EnvoyMetricsService: &meshconfig.RemoteService{Address: "metrics-service.dubbo-system:15000"},
EnvoyAccessLogService: &meshconfig.RemoteService{Address: "accesslog-service.dubbo-system:15000"},
ControlPlaneAuthPolicy: meshconfig.AuthenticationPolicy_MUTUAL_TLS,
Tracing: nil,
StatusPort: 15020,
PrivateKeyProvider: nil,
}
modify := func(config *meshconfig.ProxyConfig, fieldSetter func(*meshconfig.ProxyConfig)) *meshconfig.ProxyConfig {
clone := proto.Clone(config).(*meshconfig.ProxyConfig)
fieldSetter(clone)
return clone
}
cases := []struct {
name string
in *meshconfig.ProxyConfig
isValid bool
}{
{
name: "empty proxy config",
in: &meshconfig.ProxyConfig{},
isValid: false,
},
{
name: "valid proxy config",
in: valid,
isValid: true,
},
{
name: "config path invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.ConfigPath = "" }),
isValid: false,
},
{
name: "binary path invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.BinaryPath = "" }),
isValid: false,
},
{
name: "discovery address invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.DiscoveryAddress = "10.0.0.100" }),
isValid: false,
},
{
name: "proxy admin port invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.ProxyAdminPort = 0 }),
isValid: false,
},
{
name: "proxy admin port invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.ProxyAdminPort = 65536 }),
isValid: false,
},
{
name: "validate status port",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.StatusPort = 0 }),
isValid: false,
},
{
name: "validate vstatus port",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.StatusPort = 65536 }),
isValid: false,
},
{
name: "drain duration invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.DrainDuration = durationpb.New(-1 * time.Second) }),
isValid: false,
},
{
name: "parent shutdown duration invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.ParentShutdownDuration = durationpb.New(-1 * time.Second) }),
isValid: false,
},
{
name: "service cluster invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) {
c.ClusterName = &meshconfig.ProxyConfig_ServiceCluster{ServiceCluster: ""}
}),
isValid: false,
},
{
name: "statsd udp address invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.StatsdUdpAddress = "10.0.0.100" }),
isValid: false,
},
{
name: "envoy metrics service address invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) {
c.EnvoyMetricsService = &meshconfig.RemoteService{Address: "metrics-service.dubbo-system"}
}),
isValid: false,
},
{
name: "envoy access log service address invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) {
c.EnvoyAccessLogService = &meshconfig.RemoteService{Address: "accesslog-service.dubbo-system"}
}),
isValid: false,
},
{
name: "control plane auth policy invalid",
in: modify(valid, func(c *meshconfig.ProxyConfig) { c.ControlPlaneAuthPolicy = -1 }),
isValid: false,
},
{
name: "zipkin address is valid",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Zipkin_{
Zipkin: &meshconfig.Tracing_Zipkin{
Address: "zipkin.dubbo-system:9411",
},
},
}
},
),
isValid: true,
},
{
name: "zipkin address with $(HOST_IP) is valid",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Zipkin_{
Zipkin: &meshconfig.Tracing_Zipkin{
Address: "$(HOST_IP):9411",
},
},
}
},
),
isValid: true,
},
{
name: "zipkin config invalid",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Zipkin_{
Zipkin: &meshconfig.Tracing_Zipkin{
Address: "10.0.0.100",
},
},
}
},
),
isValid: false,
},
{
name: "lightstep config is valid",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Lightstep_{
Lightstep: &meshconfig.Tracing_Lightstep{
Address: "collector.lightstep:8080",
AccessToken: "abcdefg1234567",
},
},
}
},
),
isValid: true,
},
{
name: "lightstep address invalid",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Lightstep_{
Lightstep: &meshconfig.Tracing_Lightstep{
Address: "10.0.0.100",
AccessToken: "abcdefg1234567",
},
},
}
},
),
isValid: false,
},
{
name: "lightstep address empty but lightstep access token is not",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Lightstep_{
Lightstep: &meshconfig.Tracing_Lightstep{
Address: "",
AccessToken: "abcdefg1234567",
},
},
}
},
),
isValid: false,
},
{
name: "lightstep address is valid but access token is empty",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Lightstep_{
Lightstep: &meshconfig.Tracing_Lightstep{
Address: "collector.lightstep:8080",
AccessToken: "",
},
},
}
},
),
isValid: false,
},
{
name: "lightstep access token empty but lightstep address is not",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Lightstep_{
Lightstep: &meshconfig.Tracing_Lightstep{
Address: "10.0.0.100",
AccessToken: "",
},
},
}
},
),
isValid: false,
},
{
name: "lightstep address and lightstep token both empty",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Lightstep_{
Lightstep: &meshconfig.Tracing_Lightstep{
Address: "",
AccessToken: "",
},
},
}
},
),
isValid: false,
},
{
name: "datadog without address",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Datadog_{
Datadog: &meshconfig.Tracing_Datadog{},
},
}
},
),
isValid: false,
},
{
name: "datadog with correct address",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Datadog_{
Datadog: &meshconfig.Tracing_Datadog{
Address: "datadog-agent:8126",
},
},
}
},
),
isValid: true,
},
{
name: "datadog with invalid address",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Datadog_{
Datadog: &meshconfig.Tracing_Datadog{
Address: "address-missing-port-number",
},
},
}
},
),
isValid: false,
},
{
name: "custom tags with a literal value",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
CustomTags: map[string]*meshconfig.Tracing_CustomTag{
"clusterID": {
Type: &meshconfig.Tracing_CustomTag_Literal{
Literal: &meshconfig.Tracing_Literal{
Value: "cluster1",
},
},
},
},
}
},
),
isValid: true,
},
{
name: "custom tags with a nil value",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.Tracing = &meshconfig.Tracing{
CustomTags: map[string]*meshconfig.Tracing_CustomTag{
"clusterID": nil,
},
}
},
),
isValid: false,
},
{
name: "private key provider with empty provider",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.PrivateKeyProvider = &meshconfig.PrivateKeyProvider{}
},
),
isValid: false,
},
{
name: "private key provider with cryptomb without poll_delay",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.PrivateKeyProvider = &meshconfig.PrivateKeyProvider{
Provider: &meshconfig.PrivateKeyProvider_Cryptomb{
Cryptomb: &meshconfig.PrivateKeyProvider_CryptoMb{},
},
}
},
),
isValid: false,
},
{
name: "private key provider with cryptomb zero poll_delay",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.PrivateKeyProvider = &meshconfig.PrivateKeyProvider{
Provider: &meshconfig.PrivateKeyProvider_Cryptomb{
Cryptomb: &meshconfig.PrivateKeyProvider_CryptoMb{
PollDelay: &durationpb.Duration{
Seconds: 0,
Nanos: 0,
},
},
},
}
},
),
isValid: false,
},
{
name: "private key provider with cryptomb",
in: modify(valid,
func(c *meshconfig.ProxyConfig) {
c.PrivateKeyProvider = &meshconfig.PrivateKeyProvider{
Provider: &meshconfig.PrivateKeyProvider_Cryptomb{
Cryptomb: &meshconfig.PrivateKeyProvider_CryptoMb{
PollDelay: &durationpb.Duration{
Seconds: 0,
Nanos: 10000,
},
},
},
}
},
),
isValid: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := ValidateMeshConfigProxyConfig(c.in); (got == nil) != c.isValid {
if c.isValid {
t.Errorf("got error %v, wanted none", got)
} else {
t.Error("got no error, wanted one")
}
}
})
}
invalid := &meshconfig.ProxyConfig{
ConfigPath: "",
BinaryPath: "",
DiscoveryAddress: "10.0.0.100",
ProxyAdminPort: 0,
DrainDuration: durationpb.New(-1 * time.Second),
ParentShutdownDuration: durationpb.New(-1 * time.Second),
ClusterName: &meshconfig.ProxyConfig_ServiceCluster{ServiceCluster: ""},
StatsdUdpAddress: "10.0.0.100",
EnvoyMetricsService: &meshconfig.RemoteService{Address: "metrics-service"},
EnvoyAccessLogService: &meshconfig.RemoteService{Address: "accesslog-service"},
ControlPlaneAuthPolicy: -1,
StatusPort: 0,
Tracing: &meshconfig.Tracing{
Tracer: &meshconfig.Tracing_Zipkin_{
Zipkin: &meshconfig.Tracing_Zipkin{
Address: "10.0.0.100",
},
},
},
}
err := ValidateMeshConfigProxyConfig(invalid)
if err == nil {
t.Errorf("expected an error on invalid proxy mesh config: %v", invalid)
} else {
switch err := err.(type) {
case *multierror.Error:
// each field must cause an error in the field
if len(err.Errors) != 13 {
t.Errorf("expected an error for each field %v", err)
}
default:
t.Errorf("expected a multi error as output")
}
}
}
func TestValidateGateway(t *testing.T) {
tests := []struct {
name string
in proto.Message
out string
warning string
}{
{"empty", &networking.Gateway{}, "server", ""},
{"invalid message", &networking.Server{}, "cannot cast", ""},
{
"happy domain",
&networking.Gateway{
Servers: []*networking.Server{{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Name: "name1", Number: 7, Protocol: "http"},
}},
},
"", "",
},
{
"happy multiple servers",
&networking.Gateway{
Servers: []*networking.Server{
{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Name: "name1", Number: 7, Protocol: "http"},
},
},
},
"", "",
},
{
"invalid port",
&networking.Gateway{
Servers: []*networking.Server{
{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Name: "name1", Number: 66000, Protocol: "http"},
},
},
},
"port", "",
},
{
"duplicate port names",
&networking.Gateway{
Servers: []*networking.Server{
{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Name: "foo", Number: 80, Protocol: "http"},
},
{
Hosts: []string{"scooby.doo.com"},
Port: &networking.Port{Name: "foo", Number: 8080, Protocol: "http"},
},
},
},
"port names", "",
},
{
"invalid domain",
&networking.Gateway{
Servers: []*networking.Server{
{
Hosts: []string{"foo.*.bar.com"},
Port: &networking.Port{Number: 7, Protocol: "http"},
},
},
},
"domain", "",
},
{
"valid httpsRedirect",
&networking.Gateway{
Servers: []*networking.Server{
{
Hosts: []string{"bar.com"},
Port: &networking.Port{Name: "http", Number: 80, Protocol: "http"},
Tls: &networking.ServerTLSSettings{HttpsRedirect: true},
},
},
},
"", "",
},
{
"invalid https httpsRedirect",
&networking.Gateway{
Servers: []*networking.Server{
{
Hosts: []string{"bar.com"},
Port: &networking.Port{Name: "https", Number: 80, Protocol: "https"},
Tls: &networking.ServerTLSSettings{HttpsRedirect: true},
},
},
},
"", "tls.httpsRedirect should only be used with http servers",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warn, err := ValidateGateway(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: tt.in,
})
checkValidationMessage(t, warn, err, tt.warning, tt.out)
})
}
}
func TestValidateServer(t *testing.T) {
tests := []struct {
name string
in *networking.Server
out string
}{
{"empty", &networking.Server{}, "host"},
{"empty", &networking.Server{}, "port"},
{
"happy",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"",
},
{
"happy ip",
&networking.Server{
Hosts: []string{"1.1.1.1"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"",
},
{
"happy ns/name",
&networking.Server{
Hosts: []string{"ns1/foo.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"",
},
{
"happy */name",
&networking.Server{
Hosts: []string{"*/foo.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"",
},
{
"happy ./name",
&networking.Server{
Hosts: []string{"./foo.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"",
},
{
"invalid domain ns/name format",
&networking.Server{
Hosts: []string{"ns1/foo.*.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"domain",
},
{
"invalid domain",
&networking.Server{
Hosts: []string{"foo.*.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"domain",
},
{
"invalid short name host",
&networking.Server{
Hosts: []string{"foo"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
},
"short names",
},
{
"invalid port",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 66000, Name: "http", Protocol: "http"},
},
"port",
},
{
"invalid tls options",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 1, Name: "http", Protocol: "http"},
Tls: &networking.ServerTLSSettings{Mode: networking.ServerTLSSettings_SIMPLE},
},
"TLS",
},
{
"no tls on HTTPS",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 10000, Name: "https", Protocol: "https"},
},
"must have TLS",
},
{
"tls on HTTP",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 10000, Name: "http", Protocol: "http"},
Tls: &networking.ServerTLSSettings{Mode: networking.ServerTLSSettings_SIMPLE},
},
"cannot have TLS",
},
{
"tls redirect on HTTP",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 10000, Name: "http", Protocol: "http"},
Tls: &networking.ServerTLSSettings{
HttpsRedirect: true,
},
},
"",
},
{
"bind ip",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
Bind: "127.0.0.1",
},
"",
},
{
"bind unix path with invalid port",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 7, Name: "http", Protocol: "http"},
Bind: "unix://@foobar",
},
"port number must be 0 for unix domain socket",
},
{
"bind unix path",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 0, Name: "http", Protocol: "http"},
Bind: "unix://@foobar",
},
"",
},
{
"bind bad ip",
&networking.Server{
Hosts: []string{"foo.bar.com"},
Port: &networking.Port{Number: 0, Name: "http", Protocol: "http"},
Bind: "foo.bar",
},
"foo.bar is not a valid IP",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := validateServer(tt.in)
warn, err := v.Unwrap()
checkValidationMessage(t, warn, err, "", tt.out)
})
}
}
func TestValidateServerPort(t *testing.T) {
tests := []struct {
name string
in *networking.Port
out string
}{
{"empty", &networking.Port{}, "invalid protocol"},
{"empty", &networking.Port{}, "port name"},
{
"happy",
&networking.Port{
Protocol: "http",
Number: 1,
Name: "Henry",
},
"",
},
{
"invalid protocol",
&networking.Port{
Protocol: "kafka",
Number: 1,
Name: "Henry",
},
"invalid protocol",
},
{
"invalid number",
&networking.Port{
Protocol: "http",
Number: uint32(1 << 30),
Name: "http",
},
"port number",
},
{
"name, no number",
&networking.Port{
Protocol: "http",
Number: 0,
Name: "Henry",
},
"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateServerPort(tt.in)
if err == nil && tt.out != "" {
t.Fatalf("validateServerPort(%v) = nil, wanted %q", tt.in, tt.out)
} else if err != nil && tt.out == "" {
t.Fatalf("validateServerPort(%v) = %v, wanted nil", tt.in, err)
} else if err != nil && !strings.Contains(err.Error(), tt.out) {
t.Fatalf("validateServerPort(%v) = %v, wanted %q", tt.in, err, tt.out)
}
})
}
}
func TestValidateTlsOptions(t *testing.T) {
tests := []struct {
name string
in *networking.ServerTLSSettings
out string
warning string
}{
{"empty", &networking.ServerTLSSettings{}, "", ""},
{
"simple",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
},
"", "",
},
{
"simple with client bundle",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
},
"", "",
},
{
"simple sds with client bundle",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
CredentialName: "sds-name",
},
"", "",
},
{
"simple no server cert",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "",
PrivateKey: "Khan Noonien Singh",
},
"server certificate", "",
},
{
"simple no private key",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "",
},
"private key", "",
},
{
"simple sds no server cert",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "",
PrivateKey: "Khan Noonien Singh",
CredentialName: "sds-name",
},
"", "",
},
{
"simple sds no private key",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "",
CredentialName: "sds-name",
},
"", "",
},
{
"mutual",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
},
"", "",
},
{
"mutual sds",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
CredentialName: "sds-name",
},
"", "",
},
{
"mutual no server cert",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
},
"server certificate", "",
},
{
"mutual sds no server cert",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
CredentialName: "sds-name",
},
"", "",
},
{
"mutual no client CA bundle",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "",
},
"client CA bundle", "",
},
// this pair asserts we get errors about both client and server certs missing when in mutual mode
// and both are absent, but requires less rewriting of the testing harness than merging the cases
{
"mutual no certs",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "",
PrivateKey: "",
CaCertificates: "",
},
"server certificate", "",
},
{
"mutual no certs",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "",
PrivateKey: "",
CaCertificates: "",
},
"private key", "",
},
{
"mutual no certs",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_MUTUAL,
ServerCertificate: "",
PrivateKey: "",
CaCertificates: "",
},
"client CA bundle", "",
},
{
"pass through sds no certs",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_PASSTHROUGH,
ServerCertificate: "",
CaCertificates: "",
CredentialName: "sds-name",
},
"", "PASSTHROUGH mode does not use certificates",
},
{
"istio_mutual no certs",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_ISTIO_MUTUAL,
ServerCertificate: "",
PrivateKey: "",
CaCertificates: "",
},
"", "",
},
{
"istio_mutual with server cert",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_ISTIO_MUTUAL,
ServerCertificate: "Captain Jean-Luc Picard",
},
"cannot have associated server cert", "",
},
{
"istio_mutual with client bundle",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_ISTIO_MUTUAL,
ServerCertificate: "Captain Jean-Luc Picard",
PrivateKey: "Khan Noonien Singh",
CaCertificates: "Commander William T. Riker",
},
"cannot have associated", "",
},
{
"istio_mutual with private key",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_ISTIO_MUTUAL,
PrivateKey: "Khan Noonien Singh",
},
"cannot have associated private key", "",
},
{
"istio_mutual with credential name",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_ISTIO_MUTUAL,
CredentialName: "some-cred",
},
"cannot have associated credentialName", "",
},
{
"invalid cipher suites",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: "sds-name",
CipherSuites: []string{"not-a-cipher-suite"},
},
"", "not-a-cipher-suite",
},
{
"valid cipher suites",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: "sds-name",
CipherSuites: []string{"ECDHE-ECDSA-AES128-SHA"},
},
"", "",
},
{
"cipher suites operations",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: "sds-name",
CipherSuites: []string{"-ECDHE-ECDSA-AES128-SHA"},
},
"", "",
},
{
"duplicate cipher suites",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: "sds-name",
CipherSuites: []string{"ECDHE-ECDSA-AES128-SHA", "ECDHE-ECDSA-AES128-SHA"},
},
"", "ECDHE-ECDSA-AES128-SHA",
},
{
"invalid cipher suites with invalid config",
&networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CipherSuites: []string{"not-a-cipher-suite"},
},
"requires a private key", "not-a-cipher-suite",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := validateTLSOptions(tt.in)
warn, err := v.Unwrap()
checkValidationMessage(t, warn, err, tt.warning, tt.out)
})
}
}
func TestValidateTLS(t *testing.T) {
testCases := []struct {
name string
tls *networking.ClientTLSSettings
valid bool
}{
{
name: "SIMPLE: Credential Name set correctly",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: "some credential",
ClientCertificate: "",
PrivateKey: "",
CaCertificates: "",
},
valid: true,
},
{
name: "SIMPLE CredentialName set with ClientCertificate specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: "credential",
ClientCertificate: "cert",
PrivateKey: "",
CaCertificates: "",
},
valid: false,
},
{
name: "SIMPLE: CredentialName set with PrivateKey specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: "credential",
ClientCertificate: "",
PrivateKey: "key",
CaCertificates: "",
},
valid: false,
},
{
name: "SIMPLE: CredentialName set with CACertficiates specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
CredentialName: "credential",
ClientCertificate: "",
PrivateKey: "",
CaCertificates: "ca",
},
valid: false,
},
{
name: "MUTUAL: Credential Name set correctly",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: "some credential",
ClientCertificate: "",
PrivateKey: "",
CaCertificates: "",
},
valid: true,
},
{
name: "MUTUAL CredentialName set with ClientCertificate specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: "credential",
ClientCertificate: "cert",
PrivateKey: "",
CaCertificates: "",
},
valid: false,
},
{
name: "MUTUAL: CredentialName set with PrivateKey specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: "credential",
ClientCertificate: "",
PrivateKey: "key",
CaCertificates: "",
},
valid: false,
},
{
name: "MUTUAL: CredentialName set with CACertficiates specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
CredentialName: "credential",
ClientCertificate: "",
PrivateKey: "",
CaCertificates: "ca",
},
valid: false,
},
{
name: "MUTUAL: CredentialName not set with ClientCertificate and Key specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: "cert",
PrivateKey: "key",
},
valid: true,
},
{
name: "MUTUAL: CredentialName not set with ClientCertificate specified and Key missing",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: "cert",
PrivateKey: "",
},
valid: false,
},
{
name: "MUTUAL: CredentialName not set with ClientCertificate missing and Key specified",
tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_MUTUAL,
ClientCertificate: "",
PrivateKey: "key",
},
valid: false,
},
}
for _, tc := range testCases {
if got := validateTLS(tc.tls); (got == nil) != tc.valid {
t.Errorf("ValidateTLS(%q) => got valid=%v, want valid=%v",
tc.name, got == nil, tc.valid)
}
}
}
func TestValidateHTTPHeaderName(t *testing.T) {
testCases := []struct {
name string
valid bool
}{
{name: "header1", valid: true},
{name: "X-Requested-With", valid: true},
{name: "", valid: false},
}
for _, tc := range testCases {
if got := ValidateHTTPHeaderName(tc.name); (got == nil) != tc.valid {
t.Errorf("ValidateHTTPHeaderName(%q) => got valid=%v, want valid=%v",
tc.name, got == nil, tc.valid)
}
}
}
func TestValidateCORSPolicy(t *testing.T) {
testCases := []struct {
name string
in *networking.CorsPolicy
valid bool
}{
{name: "valid", in: &networking.CorsPolicy{
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"header1", "header2"},
ExposeHeaders: []string{"header3"},
MaxAge: &durationpb.Duration{Seconds: 2},
}, valid: true},
{name: "bad method", in: &networking.CorsPolicy{
AllowMethods: []string{"GET", "PUTT"},
AllowHeaders: []string{"header1", "header2"},
ExposeHeaders: []string{"header3"},
MaxAge: &durationpb.Duration{Seconds: 2},
}, valid: false},
{name: "bad header", in: &networking.CorsPolicy{
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"header1", "header2"},
ExposeHeaders: []string{""},
MaxAge: &durationpb.Duration{Seconds: 2},
}, valid: false},
{name: "bad max age", in: &networking.CorsPolicy{
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"header1", "header2"},
ExposeHeaders: []string{"header3"},
MaxAge: &durationpb.Duration{Seconds: 2, Nanos: 42},
}, valid: false},
{name: "empty matchType AllowOrigins", in: &networking.CorsPolicy{
AllowOrigins: []*networking.StringMatch{
{MatchType: &networking.StringMatch_Exact{Exact: ""}},
{MatchType: &networking.StringMatch_Prefix{Prefix: ""}},
{MatchType: &networking.StringMatch_Regex{Regex: ""}},
},
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"header1", "header2"},
ExposeHeaders: []string{"header3"},
MaxAge: &durationpb.Duration{Seconds: 2},
}, valid: false},
{name: "non empty matchType AllowOrigins", in: &networking.CorsPolicy{
AllowOrigins: []*networking.StringMatch{
{MatchType: &networking.StringMatch_Exact{Exact: "exact"}},
{MatchType: &networking.StringMatch_Prefix{Prefix: "prefix"}},
{MatchType: &networking.StringMatch_Regex{Regex: "regex"}},
},
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"header1", "header2"},
ExposeHeaders: []string{"header3"},
MaxAge: &durationpb.Duration{Seconds: 2},
}, valid: true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := validateCORSPolicy(tc.in); (got == nil) != tc.valid {
t.Errorf("got valid=%v, want valid=%v: %v",
got == nil, tc.valid, got)
}
})
}
}
func TestValidateHTTPStatus(t *testing.T) {
testCases := []struct {
in int32
valid bool
}{
{-100, false},
{0, false},
{200, true},
{600, true},
{601, false},
}
for _, tc := range testCases {
if got := validateHTTPStatus(tc.in); (got == nil) != tc.valid {
t.Errorf("validateHTTPStatus(%d) => got valid=%v, want valid=%v",
tc.in, got, tc.valid)
}
}
}
func TestValidateHTTPFaultInjectionAbort(t *testing.T) {
testCases := []struct {
name string
in *networking.HTTPFaultInjection_Abort
valid bool
}{
{name: "nil", in: nil, valid: true},
{name: "valid", in: &networking.HTTPFaultInjection_Abort{
Percentage: &networking.Percent{
Value: 20,
},
ErrorType: &networking.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 200,
},
}, valid: true},
{name: "valid default", in: &networking.HTTPFaultInjection_Abort{
ErrorType: &networking.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 200,
},
}, valid: true},
{name: "invalid http status", in: &networking.HTTPFaultInjection_Abort{
Percentage: &networking.Percent{
Value: 20,
},
ErrorType: &networking.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 9000,
},
}, valid: false},
{name: "invalid low http status", in: &networking.HTTPFaultInjection_Abort{
Percentage: &networking.Percent{
Value: 20,
},
ErrorType: &networking.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 100,
},
}, valid: false},
{name: "valid percentage", in: &networking.HTTPFaultInjection_Abort{
Percentage: &networking.Percent{
Value: 0.001,
},
ErrorType: &networking.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 200,
},
}, valid: true},
{name: "invalid fractional percent", in: &networking.HTTPFaultInjection_Abort{
Percentage: &networking.Percent{
Value: -10.0,
},
ErrorType: &networking.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 200,
},
}, valid: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := validateHTTPFaultInjectionAbort(tc.in); (got == nil) != tc.valid {
t.Errorf("got valid=%v, want valid=%v: %v",
got == nil, tc.valid, got)
}
})
}
}
func TestValidateHTTPFaultInjectionDelay(t *testing.T) {
testCases := []struct {
name string
in *networking.HTTPFaultInjection_Delay
valid bool
}{
{name: "nil", in: nil, valid: true},
{name: "valid fixed", in: &networking.HTTPFaultInjection_Delay{
Percentage: &networking.Percent{
Value: 20,
},
HttpDelayType: &networking.HTTPFaultInjection_Delay_FixedDelay{
FixedDelay: &durationpb.Duration{Seconds: 3},
},
}, valid: true},
{name: "valid default", in: &networking.HTTPFaultInjection_Delay{
HttpDelayType: &networking.HTTPFaultInjection_Delay_FixedDelay{
FixedDelay: &durationpb.Duration{Seconds: 3},
},
}, valid: true},
{name: "invalid percent", in: &networking.HTTPFaultInjection_Delay{
Percentage: &networking.Percent{
Value: 101,
},
HttpDelayType: &networking.HTTPFaultInjection_Delay_FixedDelay{
FixedDelay: &durationpb.Duration{Seconds: 3},
},
}, valid: false},
{name: "invalid delay", in: &networking.HTTPFaultInjection_Delay{
Percentage: &networking.Percent{
Value: 20,
},
HttpDelayType: &networking.HTTPFaultInjection_Delay_FixedDelay{
FixedDelay: &durationpb.Duration{Seconds: 3, Nanos: 42},
},
}, valid: false},
{name: "valid fractional percentage", in: &networking.HTTPFaultInjection_Delay{
Percentage: &networking.Percent{
Value: 0.001,
},
HttpDelayType: &networking.HTTPFaultInjection_Delay_FixedDelay{
FixedDelay: &durationpb.Duration{Seconds: 3},
},
}, valid: true},
{name: "invalid fractional percentage", in: &networking.HTTPFaultInjection_Delay{
Percentage: &networking.Percent{
Value: -10.0,
},
HttpDelayType: &networking.HTTPFaultInjection_Delay_FixedDelay{
FixedDelay: &durationpb.Duration{Seconds: 3},
},
}, valid: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := validateHTTPFaultInjectionDelay(tc.in); (got == nil) != tc.valid {
t.Errorf("got valid=%v, want valid=%v: %v",
got == nil, tc.valid, got)
}
})
}
}
func TestValidateHTTPRetry(t *testing.T) {
testCases := []struct {
name string
in *networking.HTTPRetry
valid bool
}{
{name: "valid", in: &networking.HTTPRetry{
Attempts: 10,
PerTryTimeout: &durationpb.Duration{Seconds: 2},
RetryOn: "5xx,gateway-error",
}, valid: true},
{name: "disable retries", in: &networking.HTTPRetry{
Attempts: 0,
}, valid: true},
{name: "invalid, retry policy configured but attempts set to zero", in: &networking.HTTPRetry{
Attempts: 0,
PerTryTimeout: &durationpb.Duration{Seconds: 2},
RetryOn: "5xx,gateway-error",
}, valid: false},
{name: "valid default", in: &networking.HTTPRetry{
Attempts: 10,
}, valid: true},
{name: "valid http status retryOn", in: &networking.HTTPRetry{
Attempts: 10,
PerTryTimeout: &durationpb.Duration{Seconds: 2},
RetryOn: "503,connect-failure",
}, valid: true},
{name: "invalid attempts", in: &networking.HTTPRetry{
Attempts: -1,
PerTryTimeout: &durationpb.Duration{Seconds: 2},
}, valid: false},
{name: "invalid timeout", in: &networking.HTTPRetry{
Attempts: 10,
PerTryTimeout: &durationpb.Duration{Seconds: 2, Nanos: 1},
}, valid: false},
{name: "timeout too small", in: &networking.HTTPRetry{
Attempts: 10,
PerTryTimeout: &durationpb.Duration{Nanos: 999},
}, valid: false},
{name: "invalid policy retryOn", in: &networking.HTTPRetry{
Attempts: 10,
PerTryTimeout: &durationpb.Duration{Seconds: 2},
RetryOn: "5xx,invalid policy",
}, valid: false},
{name: "invalid http status retryOn", in: &networking.HTTPRetry{
Attempts: 10,
PerTryTimeout: &durationpb.Duration{Seconds: 2},
RetryOn: "600,connect-failure",
}, valid: false},
{name: "invalid, retryRemoteLocalities configured but attempts set to zero", in: &networking.HTTPRetry{
Attempts: 0,
RetryRemoteLocalities: &wrapperspb.BoolValue{Value: false},
}, valid: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := validateHTTPRetry(tc.in); (got == nil) != tc.valid {
t.Errorf("got valid=%v, want valid=%v: %v",
got == nil, tc.valid, got)
}
})
}
}
func TestValidateHTTPRewrite(t *testing.T) {
testCases := []struct {
name string
in *networking.HTTPRewrite
valid bool
}{
{
name: "nil in",
in: nil,
valid: true,
},
{
name: "uri and authority",
in: &networking.HTTPRewrite{
Uri: "/path/to/resource",
Authority: "foobar.org",
},
valid: true,
},
{
name: "uri",
in: &networking.HTTPRewrite{
Uri: "/path/to/resource",
},
valid: true,
},
{
name: "authority",
in: &networking.HTTPRewrite{
Authority: "foobar.org",
},
valid: true,
},
{
name: "no uri or authority",
in: &networking.HTTPRewrite{},
valid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := validateHTTPRewrite(tc.in); (got == nil) != tc.valid {
t.Errorf("got valid=%v, want valid=%v: %v",
got == nil, tc.valid, got)
}
})
}
}
func TestValidatePortName(t *testing.T) {
testCases := []struct {
name string
valid bool
}{
{
name: "",
valid: false,
},
{
name: "simple",
valid: true,
},
{
name: "full",
valid: true,
},
{
name: "toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong",
valid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := ValidatePortName(tc.name); (err == nil) != tc.valid {
t.Fatalf("got valid=%v but wanted valid=%v: %v", err == nil, tc.valid, err)
}
})
}
}
func TestValidateHTTPRedirect(t *testing.T) {
testCases := []struct {
name string
redirect *networking.HTTPRedirect
valid bool
}{
{
name: "nil redirect",
redirect: nil,
valid: true,
},
{
name: "empty uri and authority",
redirect: &networking.HTTPRedirect{
Uri: "",
Authority: "",
},
valid: false,
},
{
name: "too small redirect code",
redirect: &networking.HTTPRedirect{
Uri: "t",
Authority: "",
RedirectCode: 299,
},
valid: false,
},
{
name: "too large redirect code",
redirect: &networking.HTTPRedirect{
Uri: "t",
Authority: "",
RedirectCode: 400,
},
valid: false,
},
{
name: "empty authority",
redirect: &networking.HTTPRedirect{
Uri: "t",
Authority: "",
},
valid: true,
},
{
name: "empty uri",
redirect: &networking.HTTPRedirect{
Uri: "",
Authority: "t",
},
valid: true,
},
{
name: "empty redirect code",
redirect: &networking.HTTPRedirect{
Uri: "t",
Authority: "t",
RedirectCode: 0,
},
valid: true,
},
{
name: "normal redirect",
redirect: &networking.HTTPRedirect{
Uri: "t",
Authority: "t",
RedirectCode: 308,
},
valid: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := validateHTTPRedirect(tc.redirect); (err == nil) != tc.valid {
t.Fatalf("got valid=%v but wanted valid=%v: %v", err == nil, tc.valid, err)
}
})
}
}
func TestValidateDestinationWithInheritance(t *testing.T) {
test.SetBoolForTest(t, &features.EnableDestinationRuleInheritance, true)
cases := []struct {
name string
in proto.Message
valid bool
}{
{name: "simple destination rule", in: &networking.DestinationRule{
Host: "reviews",
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
Subsets: []*networking.Subset{
{Name: "v1", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: true},
{name: "simple global destination rule", in: &networking.DestinationRule{
Host: "reviews",
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
}, valid: true},
{name: "global rule with subsets", in: &networking.DestinationRule{
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
Subsets: []*networking.Subset{
{Name: "v1", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: false},
{name: "global rule with exportTo", in: &networking.DestinationRule{
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
ExportTo: []string{"ns1", "ns2"},
}, valid: false},
{name: "empty host with workloadSelector", in: &networking.DestinationRule{
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
},
WorkloadSelector: &api.WorkloadSelector{
MatchLabels: map[string]string{"app": "app1"},
},
}, valid: false},
{name: "global rule with portLevelSettings", in: &networking.DestinationRule{
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.ClientTLSSettings{
Mode: networking.ClientTLSSettings_SIMPLE,
},
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
Port: &networking.PortSelector{Number: 8000},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
},
},
}, valid: false},
}
for _, c := range cases {
if _, got := ValidateDestinationRule(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: c.in,
}); (got == nil) != c.valid {
t.Errorf("ValidateDestinationRule failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got == nil, c.valid, got)
}
}
}
func TestValidateDestination(t *testing.T) {
testCases := []struct {
name string
destination *networking.Destination
valid bool
}{
{
name: "empty",
destination: &networking.Destination{}, // nothing
valid: false,
},
{
name: "simple",
destination: &networking.Destination{
Host: "foo.bar",
},
valid: true,
},
{
name: "full",
destination: &networking.Destination{
Host: "foo.bar",
Subset: "shiny",
Port: &networking.PortSelector{
Number: 5000,
},
},
valid: true,
},
{
name: "unnumbered-selector",
destination: &networking.Destination{
Host: "foo.bar",
Subset: "shiny",
Port: &networking.PortSelector{},
},
valid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := validateDestination(tc.destination); (err == nil) != tc.valid {
t.Fatalf("got valid=%v but wanted valid=%v: %v", err == nil, tc.valid, err)
}
})
}
}
func TestValidateHTTPRoute(t *testing.T) {
testCases := []struct {
name string
route *networking.HTTPRoute
valid bool
}{
{name: "empty", route: &networking.HTTPRoute{ // nothing
}, valid: false},
{name: "simple", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}, valid: true},
{name: "no destination", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: nil,
}},
}, valid: false},
{name: "weighted", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 25,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 75,
}},
}, valid: true},
{name: "total weight > 100", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 55,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 50,
}},
}, valid: false},
{name: "total weight < 100", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 49,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 50,
}},
}, valid: false},
{name: "simple redirect", route: &networking.HTTPRoute{
Redirect: &networking.HTTPRedirect{
Uri: "/lerp",
Authority: "foo.biz",
},
}, valid: true},
{name: "conflicting redirect and route", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
Redirect: &networking.HTTPRedirect{
Uri: "/lerp",
Authority: "foo.biz",
},
}, valid: false},
{name: "request response headers", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}, valid: true},
{name: "valid headers", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{
Add: map[string]string{
"name": "",
},
Set: map[string]string{
"name": "",
},
Remove: []string{
"name",
},
},
Response: &networking.Headers_HeaderOperations{
Add: map[string]string{
"name": "",
},
Set: map[string]string{
"name": "",
},
Remove: []string{
"name",
},
},
},
}},
}, valid: true},
{name: "empty header name - request add", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{
Add: map[string]string{
"": "value",
},
},
},
}},
}, valid: false},
{name: "empty header name - request set", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{
Set: map[string]string{
"": "value",
},
},
},
}},
}, valid: false},
{name: "empty header name - request remove", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{
Remove: []string{
"",
},
},
},
}},
}, valid: false},
{name: "empty header name - response add", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Add: map[string]string{
"": "value",
},
},
},
}},
}, valid: false},
{name: "empty header name - response set", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Set: map[string]string{
"": "value",
},
},
},
}},
}, valid: false},
{name: "empty header name - response remove", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Remove: []string{
"",
},
},
},
}},
}, valid: false},
{name: "envoy escaped % set", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Set: map[string]string{
"i-love-istio": "100%%",
},
},
},
}},
}, valid: true},
{name: "envoy variable set", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Set: map[string]string{
"name": "%HOSTNAME%",
},
},
},
}},
}, valid: true},
{name: "envoy unescaped % set", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Set: map[string]string{
"name": "abcd%oijasodifj",
},
},
},
}},
}, valid: false},
{name: "envoy escaped % add", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Add: map[string]string{
"i-love-istio": "100%% and more",
},
},
},
}},
}, valid: true},
{name: "envoy variable add", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Add: map[string]string{
"name": "hello %HOSTNAME%",
},
},
},
}},
}, valid: true},
{name: "envoy unescaped % add", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Response: &networking.Headers_HeaderOperations{
Add: map[string]string{
"name": "abcd%oijasodifj",
},
},
},
}},
}, valid: false},
{name: "null header match", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: []*networking.HTTPMatchRequest{{
Headers: map[string]*networking.StringMatch{
"header": nil,
},
}},
}, valid: false},
{name: "nil match", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: nil,
}, valid: true},
{name: "match with nil element", route: &networking.HTTPRoute{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: []*networking.HTTPMatchRequest{nil},
}, valid: true},
{name: "invalid mirror percent", route: &networking.HTTPRoute{
MirrorPercent: &wrapperspb.UInt32Value{Value: 101},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: []*networking.HTTPMatchRequest{nil},
}, valid: false},
{name: "invalid mirror percentage", route: &networking.HTTPRoute{
MirrorPercentage: &networking.Percent{
Value: 101,
},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: []*networking.HTTPMatchRequest{nil},
}, valid: false},
{name: "valid mirror percentage", route: &networking.HTTPRoute{
MirrorPercentage: &networking.Percent{
Value: 1,
},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: []*networking.HTTPMatchRequest{nil},
}, valid: true},
{name: "negative mirror percentage", route: &networking.HTTPRoute{
MirrorPercentage: &networking.Percent{
Value: -1,
},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.bar"},
}},
Match: []*networking.HTTPMatchRequest{nil},
}, valid: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := validateHTTPRoute(tc.route, false); (err.Err == nil) != tc.valid {
t.Fatalf("got valid=%v but wanted valid=%v: %v", err.Err == nil, tc.valid, err)
}
})
}
}
func TestValidateRouteDestination(t *testing.T) {
testCases := []struct {
name string
routes []*networking.RouteDestination
valid bool
}{
{name: "simple", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}}, valid: true},
{name: "wildcard dash", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "*-foo.baz"},
}}, valid: true},
{name: "wildcard prefix", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "*foo.baz"},
}}, valid: true},
{name: "wildcard", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "*"},
}}, valid: false},
{name: "bad wildcard", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.*"},
}}, valid: false},
{name: "bad fqdn", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "default/baz"},
}}, valid: false},
{name: "no destination", routes: []*networking.RouteDestination{{
Destination: nil,
}}, valid: false},
{name: "weighted", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 25,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 75,
}}, valid: true},
{name: "weight < 0", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 5,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: -1,
}}, valid: false},
{name: "total weight > 100", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 55,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 50,
}}, valid: false},
{name: "total weight < 100", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 49,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 50,
}}, valid: false},
{name: "total weight = 100", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 100,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 0,
}}, valid: true},
{name: "weight = 0", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 0,
}}, valid: true},
{name: "total weight = 0 with multi RouteDestination", routes: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz.south"},
Weight: 0,
}, {
Destination: &networking.Destination{Host: "foo.baz.east"},
Weight: 0,
}}, valid: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := validateRouteDestinations(tc.routes); (err == nil) != tc.valid {
t.Fatalf("got valid=%v but wanted valid=%v: %v", err == nil, tc.valid, err)
}
})
}
}
// TODO: add TCP test cases once it is implemented
func TestValidateVirtualService(t *testing.T) {
testCases := []struct {
name string
in proto.Message
valid bool
warning bool
}{
{name: "simple", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true},
{name: "duplicate hosts", in: &networking.VirtualService{
Hosts: []string{"*.foo.bar", "*.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false},
{name: "with no destination", in: &networking.VirtualService{
Hosts: []string{"*.foo.bar", "*.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{}},
}},
}, valid: false},
{name: "destination with out hosts", in: &networking.VirtualService{
Hosts: []string{"*.foo.bar", "*.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{},
}},
}},
}, valid: false},
{name: "delegate with no hosts", in: &networking.VirtualService{
Hosts: nil,
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true},
{name: "bad host", in: &networking.VirtualService{
Hosts: []string{"foo.ba!r"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false},
{name: "no tcp or http routing", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
}, valid: false},
{name: "bad gateway", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"b@dgateway"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false},
{name: "FQDN for gateway", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"gateway.example.com"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true, warning: true},
{name: "namespace/name for gateway", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"ns1/gateway"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true},
{name: "namespace/* for gateway", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"ns1/*"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false},
{name: "*/name for gateway", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"*/gateway"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false},
{name: "wildcard for mesh gateway", in: &networking.VirtualService{
Hosts: []string{"*"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false},
{name: "wildcard for non-mesh gateway", in: &networking.VirtualService{
Hosts: []string{"*"},
Gateways: []string{"somegateway"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true},
{name: "missing tcp route", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Tcp: []*networking.TCPRoute{{
Match: []*networking.L4MatchAttributes{
{Port: 999},
},
}},
}, valid: false},
{name: "missing tls route", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Tls: []*networking.TLSRoute{{
Match: []*networking.TLSMatchAttributes{
{
Port: 999,
SniHosts: []string{"foo.bar"},
},
},
}},
}, valid: false},
{name: "deprecated mirror", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"ns1/gateway"},
Http: []*networking.HTTPRoute{{
MirrorPercent: &wrapperspb.UInt32Value{Value: 5},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true, warning: true},
{name: "set authority", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Http: []*networking.HTTPRoute{{
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{Set: map[string]string{":authority": "foo"}},
},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: true, warning: false},
{name: "set authority in destination", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{Set: map[string]string{":authority": "foo"}},
},
}},
}},
}, valid: false, warning: false},
{name: "set authority in rewrite and header", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Http: []*networking.HTTPRoute{{
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{Set: map[string]string{":authority": "foo"}},
},
Rewrite: &networking.HTTPRewrite{Authority: "bar"},
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
}},
}, valid: false, warning: false},
{name: "non-method-get", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Prefix{Prefix: "/api/v1/product"},
},
},
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Prefix{Prefix: "/api/v1/products"},
},
Method: &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{Exact: "GET"},
},
},
},
}},
}, valid: true, warning: true},
{name: "uri-with-prefix-exact", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Prefix{Prefix: "/"},
},
},
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{Exact: "/"},
},
Method: &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{Exact: "GET"},
},
},
},
}},
}, valid: true, warning: false},
{name: "jwt claim route without gateway", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Gateways: []string{"mesh"},
Http: []*networking.HTTPRoute{{
Route: []*networking.HTTPRouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Prefix{Prefix: "/"},
},
Headers: map[string]*networking.StringMatch{
"@request.auth.claims.foo": {
MatchType: &networking.StringMatch_Exact{Exact: "bar"},
},
},
},
},
}},
}, valid: false, warning: false},
{name: "ip address as sni host", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Tls: []*networking.TLSRoute{{
Route: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
Match: []*networking.TLSMatchAttributes{
{
Port: 999,
SniHosts: []string{"1.1.1.1"},
},
},
}},
}, valid: true, warning: true},
{name: "invalid wildcard as sni host", in: &networking.VirtualService{
Hosts: []string{"foo.bar"},
Tls: []*networking.TLSRoute{{
Route: []*networking.RouteDestination{{
Destination: &networking.Destination{Host: "foo.baz"},
}},
Match: []*networking.TLSMatchAttributes{
{
Port: 999,
SniHosts: []string{"foo.*.com"},
},
},
}},
}, valid: false, warning: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
warn, err := ValidateVirtualService(config.Config{Spec: tc.in})
checkValidation(t, warn, err, tc.valid, tc.warning)
})
}
}
func TestValidateWorkloadEntry(t *testing.T) {
testCases := []struct {
name string
in proto.Message
valid bool
warning bool
}{
{
name: "valid",
in: &networking.WorkloadEntry{Address: "1.2.3.4"},
valid: true,
},
{
name: "missing address",
in: &networking.WorkloadEntry{},
valid: false,
},
{
name: "valid unix endpoint",
in: &networking.WorkloadEntry{Address: "unix:///lon/google/com"},
valid: true,
},
{
name: "invalid unix endpoint",
in: &networking.WorkloadEntry{Address: "unix:///lon/google/com", Ports: map[string]uint32{"7777": 7777}},
valid: false,
},
{
name: "valid FQDN",
in: &networking.WorkloadEntry{Address: "validdns.com", Ports: map[string]uint32{"7777": 7777}},
valid: true,
},
{
name: "invalid FQDN",
in: &networking.WorkloadEntry{Address: "invaliddns.com:9443", Ports: map[string]uint32{"7777": 7777}},
valid: false,
},
{
name: "valid IP",
in: &networking.WorkloadEntry{Address: "172.16.1.1", Ports: map[string]uint32{"7777": 7777}},
valid: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
warn, err := ValidateWorkloadEntry(config.Config{Spec: tc.in})
checkValidation(t, warn, err, tc.valid, tc.warning)
})
}
}
func TestValidateWorkloadGroup(t *testing.T) {
testCases := []struct {
name string
in proto.Message
valid bool
warning bool
}{
{
name: "valid",
in: &networking.WorkloadGroup{Template: &networking.WorkloadEntry{}},
valid: true,
},
{
name: "invalid",
in: &networking.WorkloadGroup{Template: &networking.WorkloadEntry{}, Metadata: &networking.WorkloadGroup_ObjectMeta{Labels: map[string]string{
".": "~",
}}},
valid: false,
},
{
name: "probe missing method",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{},
},
valid: false,
},
{
name: "probe nil",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_HttpGet{},
},
},
valid: false,
},
{
name: "probe http empty",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_HttpGet{
HttpGet: &networking.HTTPHealthCheckConfig{},
},
},
},
valid: false,
},
{
name: "probe http valid",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_HttpGet{
HttpGet: &networking.HTTPHealthCheckConfig{
Port: 5,
},
},
},
},
valid: true,
},
{
name: "probe tcp invalid",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_TcpSocket{
TcpSocket: &networking.TCPHealthCheckConfig{},
},
},
},
valid: false,
},
{
name: "probe tcp valid",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_TcpSocket{
TcpSocket: &networking.TCPHealthCheckConfig{
Port: 5,
},
},
},
},
valid: true,
},
{
name: "probe exec invalid",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_Exec{
Exec: &networking.ExecHealthCheckConfig{},
},
},
},
valid: false,
},
{
name: "probe exec valid",
in: &networking.WorkloadGroup{
Template: &networking.WorkloadEntry{},
Probe: &networking.ReadinessProbe{
HealthCheckMethod: &networking.ReadinessProbe_Exec{
Exec: &networking.ExecHealthCheckConfig{
Command: []string{"foo", "bar"},
},
},
},
},
valid: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
warn, err := ValidateWorkloadGroup(config.Config{Spec: tc.in})
checkValidation(t, warn, err, tc.valid, tc.warning)
})
}
}
func checkValidation(t *testing.T, gotWarning Warning, gotError error, valid bool, warning bool) {
t.Helper()
if (gotError == nil) != valid {
t.Fatalf("got valid=%v but wanted valid=%v: %v", gotError == nil, valid, gotError)
}
if (gotWarning == nil) == warning {
t.Fatalf("got warning=%v but wanted warning=%v", gotWarning, warning)
}
}
func stringOrEmpty(v error) string {
if v == nil {
return ""
}
return v.Error()
}
func checkValidationMessage(t *testing.T, gotWarning Warning, gotError error, wantWarning string, wantError string) {
t.Helper()
if (gotError == nil) != (wantError == "") {
t.Fatalf("got err=%v but wanted err=%v", gotError, wantError)
}
if !strings.Contains(stringOrEmpty(gotError), wantError) {
t.Fatalf("got err=%v but wanted err=%v", gotError, wantError)
}
if (gotWarning == nil) != (wantWarning == "") {
t.Fatalf("got warning=%v but wanted warning=%v", gotWarning, wantWarning)
}
if !strings.Contains(stringOrEmpty(gotWarning), wantWarning) {
t.Fatalf("got warning=%v but wanted warning=%v", gotWarning, wantWarning)
}
}
func TestValidateDestinationRule(t *testing.T) {
cases := []struct {
name string
in proto.Message
valid bool
}{
{name: "simple destination rule", in: &networking.DestinationRule{
Host: "reviews",
Subsets: []*networking.Subset{
{Name: "v1", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: true},
{name: "missing destination name", in: &networking.DestinationRule{
Host: "",
Subsets: []*networking.Subset{
{Name: "v1", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: false},
{name: "missing subset name", in: &networking.DestinationRule{
Host: "reviews",
Subsets: []*networking.Subset{
{Name: "", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: false},
{name: "valid traffic policy, top level", in: &networking.DestinationRule{
Host: "reviews",
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
Subsets: []*networking.Subset{
{Name: "v1", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: true},
{name: "invalid traffic policy, top level", in: &networking.DestinationRule{
Host: "reviews",
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
Subsets: []*networking.Subset{
{Name: "v1", Labels: map[string]string{"version": "v1"}},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: false},
{name: "valid traffic policy, subset level", in: &networking.DestinationRule{
Host: "reviews",
Subsets: []*networking.Subset{
{
Name: "v1", Labels: map[string]string{"version": "v1"},
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: true},
{name: "invalid traffic policy, subset level", in: &networking.DestinationRule{
Host: "reviews",
Subsets: []*networking.Subset{
{
Name: "v1", Labels: map[string]string{"version": "v1"},
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: false},
{name: "valid traffic policy, both levels", in: &networking.DestinationRule{
Host: "reviews",
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
Subsets: []*networking.Subset{
{
Name: "v1", Labels: map[string]string{"version": "v1"},
TrafficPolicy: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 30,
},
},
},
{Name: "v2", Labels: map[string]string{"version": "v2"}},
},
}, valid: true},
}
for _, c := range cases {
if _, got := ValidateDestinationRule(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: c.in,
}); (got == nil) != c.valid {
t.Errorf("ValidateDestinationRule failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got == nil, c.valid, got)
}
}
}
func TestValidateTrafficPolicy(t *testing.T) {
cases := []struct {
name string
in *networking.TrafficPolicy
valid bool
}{
{
name: "valid traffic policy", in: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
valid: true,
},
{
name: "invalid traffic policy, nil entries", in: &networking.TrafficPolicy{},
valid: false,
},
{
name: "invalid traffic policy, missing port in port level settings", in: &networking.TrafficPolicy{
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{
{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
},
},
valid: false,
},
{
name: "invalid traffic policy, bad connection pool", in: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: 20,
},
},
valid: false,
},
{
name: "invalid traffic policy, panic threshold too low", in: &networking.TrafficPolicy{
LoadBalancer: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
ConnectionPool: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: 7},
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: 11},
},
OutlierDetection: &networking.OutlierDetection{
MinHealthPercent: -1,
},
},
valid: false,
},
{
name: "invalid traffic policy, both upgrade and use client protocol set", in: &networking.TrafficPolicy{
ConnectionPool: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_UPGRADE,
UseClientProtocol: true,
},
},
},
valid: false,
},
}
for _, c := range cases {
if got := validateTrafficPolicy(c.in).Err; (got == nil) != c.valid {
t.Errorf("ValidateTrafficPolicy failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got == nil, c.valid, got)
}
}
}
func TestValidateConnectionPool(t *testing.T) {
cases := []struct {
name string
in *networking.ConnectionPoolSettings
valid bool
}{
{
name: "valid connection pool, tcp and http", in: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{
MaxConnections: 7,
ConnectTimeout: &durationpb.Duration{Seconds: 2},
},
Http: &networking.ConnectionPoolSettings_HTTPSettings{
Http1MaxPendingRequests: 2,
Http2MaxRequests: 11,
MaxRequestsPerConnection: 5,
MaxRetries: 4,
IdleTimeout: &durationpb.Duration{Seconds: 30},
},
},
valid: true,
},
{
name: "valid connection pool, tcp only", in: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{
MaxConnections: 7,
ConnectTimeout: &durationpb.Duration{Seconds: 2},
},
},
valid: true,
},
{
name: "valid connection pool, http only", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
Http1MaxPendingRequests: 2,
Http2MaxRequests: 11,
MaxRequestsPerConnection: 5,
MaxRetries: 4,
IdleTimeout: &durationpb.Duration{Seconds: 30},
},
},
valid: true,
},
{
name: "valid connection pool, http only with empty idle timeout", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{
Http1MaxPendingRequests: 2,
Http2MaxRequests: 11,
MaxRequestsPerConnection: 5,
MaxRetries: 4,
},
},
valid: true,
},
{name: "invalid connection pool, empty", in: &networking.ConnectionPoolSettings{}, valid: false},
{
name: "invalid connection pool, bad max connections", in: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{MaxConnections: -1},
},
valid: false,
},
{
name: "invalid connection pool, bad connect timeout", in: &networking.ConnectionPoolSettings{
Tcp: &networking.ConnectionPoolSettings_TCPSettings{
ConnectTimeout: &durationpb.Duration{Seconds: 2, Nanos: 5},
},
},
valid: false,
},
{
name: "invalid connection pool, bad max pending requests", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http1MaxPendingRequests: -1},
},
valid: false,
},
{
name: "invalid connection pool, bad max requests", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{Http2MaxRequests: -1},
},
valid: false,
},
{
name: "invalid connection pool, bad max requests per connection", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{MaxRequestsPerConnection: -1},
},
valid: false,
},
{
name: "invalid connection pool, bad max retries", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{MaxRetries: -1},
},
valid: false,
},
{
name: "invalid connection pool, bad idle timeout", in: &networking.ConnectionPoolSettings{
Http: &networking.ConnectionPoolSettings_HTTPSettings{IdleTimeout: &durationpb.Duration{Seconds: 30, Nanos: 5}},
},
valid: false,
},
}
for _, c := range cases {
if got := validateConnectionPool(c.in); (got == nil) != c.valid {
t.Errorf("ValidateConnectionSettings failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got == nil, c.valid, got)
}
}
}
func TestValidateLoadBalancer(t *testing.T) {
duration := durationpb.Duration{Seconds: int64(time.Hour / time.Second)}
cases := []struct {
name string
in *networking.LoadBalancerSettings
valid bool
}{
{
name: "valid load balancer with simple load balancing", in: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_Simple{
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
},
},
valid: true,
},
{
name: "valid load balancer with consistentHash load balancing", in: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
MinimumRingSize: 1024,
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
Name: "test",
Ttl: &duration,
},
},
},
},
},
valid: true,
},
{
name: "invalid load balancer with consistentHash load balancing, missing ttl", in: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
MinimumRingSize: 1024,
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
Name: "test",
},
},
},
},
},
valid: false,
},
{
name: "invalid load balancer with consistentHash load balancing, missing name", in: &networking.LoadBalancerSettings{
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
MinimumRingSize: 1024,
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
Ttl: &duration,
},
},
},
},
},
valid: false,
},
}
for _, c := range cases {
if got := validateLoadBalancer(c.in); (got == nil) != c.valid {
t.Errorf("validateLoadBalancer failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got == nil, c.valid, got)
}
}
}
func TestValidateOutlierDetection(t *testing.T) {
cases := []struct {
name string
in *networking.OutlierDetection
valid bool
warn bool
}{
{name: "valid outlier detection", in: &networking.OutlierDetection{
Interval: &durationpb.Duration{Seconds: 2},
BaseEjectionTime: &durationpb.Duration{Seconds: 2},
MaxEjectionPercent: 50,
}, valid: true},
{
name: "invalid outlier detection, bad interval", in: &networking.OutlierDetection{
Interval: &durationpb.Duration{Seconds: 2, Nanos: 5},
},
valid: false,
},
{
name: "invalid outlier detection, bad base ejection time", in: &networking.OutlierDetection{
BaseEjectionTime: &durationpb.Duration{Seconds: 2, Nanos: 5},
},
valid: false,
},
{
name: "invalid outlier detection, bad max ejection percent", in: &networking.OutlierDetection{
MaxEjectionPercent: 105,
},
valid: false,
},
{
name: "invalid outlier detection, panic threshold too low", in: &networking.OutlierDetection{
MinHealthPercent: -1,
},
valid: false,
},
{
name: "invalid outlier detection, panic threshold too high", in: &networking.OutlierDetection{
MinHealthPercent: 101,
},
valid: false,
},
{
name: "deprecated outlier detection, ConsecutiveErrors", in: &networking.OutlierDetection{
ConsecutiveErrors: 101,
},
valid: true,
warn: true,
},
{
name: "consecutive local origin errors is set but split local origin errors is not set", in: &networking.OutlierDetection{
ConsecutiveLocalOriginFailures: &wrapperspb.UInt32Value{Value: 10},
},
valid: false,
},
}
for _, c := range cases {
got := validateOutlierDetection(c.in)
if (got.Err == nil) != c.valid {
t.Errorf("ValidateOutlierDetection failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got.Err == nil, c.valid, got.Err)
}
if (got.Warning == nil) == c.warn {
t.Errorf("ValidateOutlierDetection failed on %v: got warn=%v but wanted warn=%v: %v",
c.name, got.Warning == nil, c.warn, got.Warning)
}
}
}
func TestValidateEnvoyFilter(t *testing.T) {
tests := []struct {
name string
in proto.Message
error string
warning string
}{
{name: "empty filters", in: &networking.EnvoyFilter{}, error: ""},
{name: "invalid applyTo", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: 0,
},
},
}, error: "Envoy filter: missing applyTo"},
{name: "nil patch", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_LISTENER,
Patch: nil,
},
},
}, error: "Envoy filter: missing patch"},
{name: "invalid patch operation", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_LISTENER,
Patch: &networking.EnvoyFilter_Patch{},
},
},
}, error: "Envoy filter: missing patch operation"},
{name: "nil patch value", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_LISTENER,
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_ADD,
},
},
},
}, error: "Envoy filter: missing patch value for non-remove operation"},
{name: "match with invalid regex", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_LISTENER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Proxy: &networking.EnvoyFilter_ProxyMatch{
ProxyVersion: "%#@~++==`24c234`",
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: invalid regex for proxy version, [error parsing regexp: invalid nested repetition operator: `++`]"},
{name: "match with valid regex", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_LISTENER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Proxy: &networking.EnvoyFilter_ProxyMatch{
ProxyVersion: `release-1\.2-23434`,
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: ""},
{name: "listener with invalid match", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_LISTENER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
Cluster: &networking.EnvoyFilter_ClusterMatch{},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: applyTo for listener class objects cannot have non listener match"},
{name: "listener with invalid filter match", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Sni: "124",
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: filter match has no name to match on"},
{name: "listener with sub filter match and invalid applyTo", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "random",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: subfilter match can be used with applyTo HTTP_FILTER only"},
{name: "listener with sub filter match and invalid filter name", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "random",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: subfilter match requires filter match with envoy.filters.network.http_connection_manager"},
{name: "listener with sub filter match and no sub filter name", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: wellknown.HTTPConnectionManager,
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: subfilter match has no name to match on"},
{name: "route configuration with invalid match", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_VIRTUAL_HOST,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
Cluster: &networking.EnvoyFilter_ClusterMatch{},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: applyTo for http route class objects cannot have non route configuration match"},
{name: "cluster with invalid match", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_CLUSTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_REMOVE,
},
},
},
}, error: "Envoy filter: applyTo for cluster class objects cannot have non cluster match"},
{name: "invalid patch value", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_CLUSTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
Cluster: &networking.EnvoyFilter_ClusterMatch{},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_ADD,
Value: &structpb.Struct{
Fields: map[string]*structpb.Value{
"name": {
Kind: &structpb.Value_BoolValue{BoolValue: false},
},
},
},
},
},
},
}, error: `Envoy filter: json: cannot unmarshal bool into Go value of type string`},
{name: "happy config", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_CLUSTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
Cluster: &networking.EnvoyFilter_ClusterMatch{},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_ADD,
Value: &structpb.Struct{
Fields: map[string]*structpb.Value{
"lb_policy": {
Kind: &structpb.Value_StringValue{StringValue: "RING_HASH"},
},
},
},
},
},
{
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Name: "envoy.tcp_proxy",
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
Value: &structpb.Struct{
Fields: map[string]*structpb.Value{
"typed_config": {
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
Fields: map[string]*structpb.Value{
"@type": {
Kind: &structpb.Value_StringValue{
StringValue: "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz",
},
},
},
}},
},
},
},
},
},
{
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Name: "envoy.tcp_proxy",
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_FIRST,
Value: &structpb.Struct{
Fields: map[string]*structpb.Value{
"typed_config": {
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
Fields: map[string]*structpb.Value{
"@type": {
Kind: &structpb.Value_StringValue{
StringValue: "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz",
},
},
},
}},
},
},
},
},
},
},
}, error: ""},
{name: "deprecated config", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Name: "envoy.tcp_proxy",
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_FIRST,
Value: &structpb.Struct{
Fields: map[string]*structpb.Value{
"typed_config": {
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
Fields: map[string]*structpb.Value{
"@type": {
Kind: &structpb.Value_StringValue{
StringValue: "type.googleapis.com/envoy.config.filter.network.ext_authz.v2.ExtAuthz",
},
},
},
}},
},
},
},
},
},
},
}, error: "", warning: "using deprecated type_url"},
{name: "deprecated type", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "envoy.http_connection_manager",
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_FIRST,
Value: &structpb.Struct{},
},
},
},
}, error: "", warning: "using deprecated filter name"},
// Regression test for https://github.com/golang/protobuf/issues/1374
{name: "duration marshal", in: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_CLUSTER,
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_ADD,
Value: &structpb.Struct{
Fields: map[string]*structpb.Value{
"dns_refresh_rate": {
Kind: &structpb.Value_StringValue{
StringValue: "500ms",
},
},
},
},
},
},
},
}, error: "", warning: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warn, err := ValidateEnvoyFilter(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: tt.in,
})
checkValidationMessage(t, warn, err, tt.warning, tt.error)
})
}
}
func TestValidateServiceEntries(t *testing.T) {
cases := []struct {
name string
in *networking.ServiceEntry
valid bool
warning bool
}{
{
name: "discovery type DNS", in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "lon.google.com", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
},
{
name: "discovery type DNS Round Robin", in: &networking.ServiceEntry{
Hosts: []string{"*.istio.io"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "api-v1.istio.io", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "api-v2.istio.io", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS_ROUND_ROBIN,
},
valid: true,
},
{
name: "discovery type DNS, label tlsMode: istio", in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "lon.google.com", Ports: map[string]uint32{"http-valid1": 8080}, Labels: map[string]string{"security.istio.io/tlsMode": "istio"}},
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}, Labels: map[string]string{"security.istio.io/tlsMode": "istio"}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
},
{
name: "discovery type DNS, one host set with IP address and https port",
in: &networking.ServiceEntry{
Hosts: []string{"httpbin.org"},
Addresses: []string{"10.10.10.10"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
{Number: 443, Protocol: "https", Name: "https"},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
warning: false,
},
{
name: "discovery type DNS, multi hosts set with IP address and https port",
in: &networking.ServiceEntry{
Hosts: []string{"httpbin.org", "wikipedia.org"},
Addresses: []string{"10.10.10.10"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
{Number: 443, Protocol: "https", Name: "https"},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
warning: true,
},
{
name: "discovery type DNS, IP address set",
in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Addresses: []string{"10.10.10.10"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "lon.google.com", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
warning: false,
},
{
name: "discovery type DNS, IP in endpoints", in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "1.1.1.1", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
},
{
name: "empty hosts", in: &networking.ServiceEntry{
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "bad hosts", in: &networking.ServiceEntry{
Hosts: []string{"-"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "full wildcard host", in: &networking.ServiceEntry{
Hosts: []string{"foo.com", "*"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "in.google.com", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "short name host", in: &networking.ServiceEntry{
Hosts: []string{"foo", "bar.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "in.google.com", Ports: map[string]uint32{"http-valid1": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
},
{
name: "undefined endpoint port", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 80, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "lon.google.com", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "in.google.com", Ports: map[string]uint32{"http-dne": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "discovery type DNS, non-FQDN endpoint", in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "*.lon.google.com", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "in.google.com", Ports: map[string]uint32{"http-dne": 9080}},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "discovery type DNS, non-FQDN host", in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "discovery type DNS, no endpoints", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: true,
},
{
name: "discovery type DNS, unix endpoint", in: &networking.ServiceEntry{
Hosts: []string{"*.google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "unix:///lon/google/com"},
},
Resolution: networking.ServiceEntry_DNS,
},
valid: false,
},
{
name: "discovery type none", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Resolution: networking.ServiceEntry_NONE,
},
valid: true,
},
{
name: "discovery type none, endpoints provided", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "lon.google.com", Ports: map[string]uint32{"http-valid1": 8080}},
},
Resolution: networking.ServiceEntry_NONE,
},
valid: false,
},
{
name: "discovery type none, cidr addresses", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16/16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Resolution: networking.ServiceEntry_NONE,
},
valid: true,
},
{
name: "discovery type static, cidr addresses with endpoints", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16/16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "1.1.1.1", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "2.2.2.2", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_STATIC,
},
valid: true,
},
{
name: "discovery type static", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "1.1.1.1", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "2.2.2.2", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_STATIC,
},
valid: true,
},
{
name: "discovery type static, FQDN in endpoints", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "google.com", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "2.2.2.2", Ports: map[string]uint32{"http-valid2": 9080}},
},
Resolution: networking.ServiceEntry_STATIC,
},
valid: false,
},
{
name: "discovery type static, missing endpoints", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Resolution: networking.ServiceEntry_STATIC,
},
valid: true,
},
{
name: "discovery type static, bad endpoint port name", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Protocol: "http", Name: "http-valid2"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "1.1.1.1", Ports: map[string]uint32{"http-valid1": 8080}},
{Address: "2.2.2.2", Ports: map[string]uint32{"http-dne": 9080}},
},
Resolution: networking.ServiceEntry_STATIC,
},
valid: false,
},
{
name: "discovery type none, conflicting port names", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-conflict"},
{Number: 8080, Protocol: "http", Name: "http-conflict"},
},
Resolution: networking.ServiceEntry_NONE,
},
valid: false,
},
{
name: "discovery type none, conflicting port numbers", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-conflict1"},
{Number: 80, Protocol: "http", Name: "http-conflict2"},
},
Resolution: networking.ServiceEntry_NONE,
},
valid: false,
},
{
name: "unix socket", in: &networking.ServiceEntry{
Hosts: []string{"uds.cluster.local"},
Ports: []*networking.Port{
{Number: 6553, Protocol: "grpc", Name: "grpc-service1"},
},
Resolution: networking.ServiceEntry_STATIC,
Endpoints: []*networking.WorkloadEntry{
{Address: "unix:///path/to/socket"},
},
},
valid: true,
},
{
name: "unix socket, relative path", in: &networking.ServiceEntry{
Hosts: []string{"uds.cluster.local"},
Ports: []*networking.Port{
{Number: 6553, Protocol: "grpc", Name: "grpc-service1"},
},
Resolution: networking.ServiceEntry_STATIC,
Endpoints: []*networking.WorkloadEntry{
{Address: "unix://./relative/path.sock"},
},
},
valid: false,
},
{
name: "unix socket, endpoint ports", in: &networking.ServiceEntry{
Hosts: []string{"uds.cluster.local"},
Ports: []*networking.Port{
{Number: 6553, Protocol: "grpc", Name: "grpc-service1"},
},
Resolution: networking.ServiceEntry_STATIC,
Endpoints: []*networking.WorkloadEntry{
{Address: "unix:///path/to/socket", Ports: map[string]uint32{"grpc-service1": 6553}},
},
},
valid: false,
},
{
name: "unix socket, multiple service ports", in: &networking.ServiceEntry{
Hosts: []string{"uds.cluster.local"},
Ports: []*networking.Port{
{Number: 6553, Protocol: "grpc", Name: "grpc-service1"},
{Number: 80, Protocol: "http", Name: "http-service2"},
},
Resolution: networking.ServiceEntry_STATIC,
Endpoints: []*networking.WorkloadEntry{
{Address: "unix:///path/to/socket"},
},
},
valid: false,
},
{
name: "empty protocol", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
Addresses: []string{"172.1.2.16/16"},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
{Number: 8080, Name: "http-valid2"},
},
Resolution: networking.ServiceEntry_NONE,
},
valid: true,
},
{
name: "selector", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
WorkloadSelector: &networking.WorkloadSelector{Labels: map[string]string{"foo": "bar"}},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
},
valid: true,
},
{
name: "selector and endpoints", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
WorkloadSelector: &networking.WorkloadSelector{Labels: map[string]string{"foo": "bar"}},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
Endpoints: []*networking.WorkloadEntry{
{Address: "1.1.1.1"},
},
},
valid: false,
},
{
name: "bad selector key", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
WorkloadSelector: &networking.WorkloadSelector{Labels: map[string]string{"": "bar"}},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1"},
},
},
valid: false,
},
{
name: "repeat target port", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
WorkloadSelector: &networking.WorkloadSelector{Labels: map[string]string{"key": "bar"}},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1", TargetPort: 80},
{Number: 81, Protocol: "http", Name: "http-valid2", TargetPort: 80},
},
},
valid: true,
},
{
name: "valid target port", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
WorkloadSelector: &networking.WorkloadSelector{Labels: map[string]string{"key": "bar"}},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1", TargetPort: 81},
},
},
valid: true,
},
{
name: "invalid target port", in: &networking.ServiceEntry{
Hosts: []string{"google.com"},
WorkloadSelector: &networking.WorkloadSelector{Labels: map[string]string{"key": "bar"}},
Ports: []*networking.Port{
{Number: 80, Protocol: "http", Name: "http-valid1", TargetPort: 65536},
},
},
valid: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
warning, err := ValidateServiceEntry(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: c.in,
})
if (err == nil) != c.valid {
t.Errorf("ValidateServiceEntry got valid=%v but wanted valid=%v: %v",
err == nil, c.valid, err)
}
if (warning != nil) != c.warning {
t.Errorf("ValidateServiceEntry got warning=%v but wanted warning=%v: %v",
warning != nil, c.warning, warning)
}
})
}
}
func TestValidateAuthorizationPolicy(t *testing.T) {
cases := []struct {
name string
annotations map[string]string
in proto.Message
valid bool
}{
{
name: "good",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
"version": "v1",
},
},
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
Principals: []string{"sa1"},
},
},
{
Source: &security_beta.Source{
Principals: []string{"sa2"},
},
},
},
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Methods: []string{"GET"},
},
},
{
Operation: &security_beta.Operation{
Methods: []string{"POST"},
},
},
},
When: []*security_beta.Condition{
{
Key: "source.ip",
Values: []string{"1.2.3.4", "5.6.7.0/24"},
},
{
Key: "request.headers[:authority]",
Values: []string{"v1", "v2"},
},
},
},
},
},
valid: true,
},
{
name: "custom-good",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_CUSTOM,
ActionDetail: &security_beta.AuthorizationPolicy_Provider{
Provider: &security_beta.AuthorizationPolicy_ExtensionProvider{
Name: "my-custom-authz",
},
},
Rules: []*security_beta.Rule{{}},
},
valid: true,
},
{
name: "custom-empty-provider",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_CUSTOM,
ActionDetail: &security_beta.AuthorizationPolicy_Provider{
Provider: &security_beta.AuthorizationPolicy_ExtensionProvider{
Name: "",
},
},
Rules: []*security_beta.Rule{{}},
},
valid: false,
},
{
name: "custom-nil-provider",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_CUSTOM,
Rules: []*security_beta.Rule{{}},
},
valid: false,
},
{
name: "custom-invalid-rule",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_CUSTOM,
ActionDetail: &security_beta.AuthorizationPolicy_Provider{
Provider: &security_beta.AuthorizationPolicy_ExtensionProvider{
Name: "my-custom-authz",
},
},
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
Namespaces: []string{"ns"},
NotNamespaces: []string{"ns"},
Principals: []string{"id"},
NotPrincipals: []string{"id"},
RequestPrincipals: []string{"req"},
NotRequestPrincipals: []string{"req"},
},
},
},
When: []*security_beta.Condition{
{
Key: "source.namespace",
Values: []string{"source.namespace1"},
NotValues: []string{"source.namespace2"},
},
{
Key: "source.principal",
Values: []string{"source.principal1"},
NotValues: []string{"source.principal2"},
},
{
Key: "request.auth.claims[a]",
Values: []string{"claims1"},
NotValues: []string{"claims2"},
},
},
},
},
},
valid: false,
},
{
name: "provider-wrong-action",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_ALLOW,
ActionDetail: &security_beta.AuthorizationPolicy_Provider{
Provider: &security_beta.AuthorizationPolicy_ExtensionProvider{
Name: "",
},
},
Rules: []*security_beta.Rule{{}},
},
valid: false,
},
{
name: "allow-rules-nil",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_ALLOW,
},
valid: true,
},
{
name: "dry-run-valid-allow",
annotations: map[string]string{"istio.io/dry-run": "true"},
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_ALLOW,
},
valid: true,
},
{
name: "dry-run-valid-deny",
annotations: map[string]string{"istio.io/dry-run": "false"},
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_DENY,
Rules: []*security_beta.Rule{{}},
},
valid: true,
},
{
name: "dry-run-invalid-value",
annotations: map[string]string{"istio.io/dry-run": "foo"},
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_ALLOW,
},
valid: false,
},
{
name: "dry-run-invalid-action-custom",
annotations: map[string]string{"istio.io/dry-run": "true"},
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_CUSTOM,
},
valid: false,
},
{
name: "dry-run-invalid-action-audit",
annotations: map[string]string{"istio.io/dry-run": "true"},
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_AUDIT,
},
valid: false,
},
{
name: "deny-rules-nil",
in: &security_beta.AuthorizationPolicy{
Action: security_beta.AuthorizationPolicy_DENY,
},
valid: false,
},
{
name: "selector-empty-value",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "",
"version": "v1",
},
},
},
valid: true,
},
{
name: "selector-empty-key",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
"": "v1",
},
},
},
valid: false,
},
{
name: "selector-wildcard-value",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin-*",
},
},
},
valid: false,
},
{
name: "selector-wildcard-key",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app-*": "httpbin",
},
},
},
valid: false,
},
{
name: "from-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{},
},
},
},
valid: false,
},
{
name: "source-nil",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{},
},
},
},
},
valid: false,
},
{
name: "source-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{},
},
},
},
},
},
valid: false,
},
{
name: "to-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{},
},
},
},
valid: false,
},
{
name: "operation-nil",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{},
},
},
},
},
valid: false,
},
{
name: "operation-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{},
},
},
},
},
},
valid: false,
},
{
name: "Principals-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
Principals: []string{"p1", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotPrincipals-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
NotPrincipals: []string{"p1", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "RequestPrincipals-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
RequestPrincipals: []string{"p1", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotRequestPrincipals-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
NotRequestPrincipals: []string{"p1", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "Namespaces-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
Namespaces: []string{"ns", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotNamespaces-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
NotNamespaces: []string{"ns", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "IpBlocks-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
IpBlocks: []string{"1.2.3.4", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotIpBlocks-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
NotIpBlocks: []string{"1.2.3.4", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "RemoteIpBlocks-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
RemoteIpBlocks: []string{"1.2.3.4", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotRemoteIpBlocks-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
NotRemoteIpBlocks: []string{"1.2.3.4", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "Hosts-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Hosts: []string{"host", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotHosts-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
NotHosts: []string{"host", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "Ports-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Ports: []string{"80", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotPorts-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
NotPorts: []string{"80", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "Methods-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Methods: []string{"GET", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotMethods-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
NotMethods: []string{"GET", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "Paths-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Paths: []string{"/path", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "NotPaths-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
NotPaths: []string{"/path", ""},
},
},
},
},
},
},
valid: false,
},
{
name: "value-empty",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Key: "request.headers[:authority]",
Values: []string{"v1", ""},
},
},
},
},
},
valid: false,
},
{
name: "invalid ip and port in ipBlocks",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
IpBlocks: []string{"1.2.3.4", "ip1"},
NotIpBlocks: []string{"5.6.7.8", "ip2"},
},
},
},
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Ports: []string{"80", "port1"},
NotPorts: []string{"90", "port2"},
},
},
},
},
},
},
valid: false,
},
{
name: "invalid ip and port in remoteIpBlocks",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
From: []*security_beta.Rule_From{
{
Source: &security_beta.Source{
RemoteIpBlocks: []string{"1.2.3.4", "ip1"},
NotRemoteIpBlocks: []string{"5.6.7.8", "ip2"},
},
},
},
To: []*security_beta.Rule_To{
{
Operation: &security_beta.Operation{
Ports: []string{"80", "port1"},
NotPorts: []string{"90", "port2"},
},
},
},
},
},
},
valid: false,
},
{
name: "condition-key-missing",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Values: []string{"v1", "v2"},
},
},
},
},
},
valid: false,
},
{
name: "condition-key-empty",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Key: "",
Values: []string{"v1", "v2"},
},
},
},
},
},
valid: false,
},
{
name: "condition-value-missing",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Key: "source.principal",
},
},
},
},
},
valid: false,
},
{
name: "condition-value-invalid",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Key: "source.ip",
Values: []string{"a.b.c.d"},
},
},
},
},
},
valid: false,
},
{
name: "condition-notValue-invalid",
in: &security_beta.AuthorizationPolicy{
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Key: "source.ip",
NotValues: []string{"a.b.c.d"},
},
},
},
},
},
valid: false,
},
{
name: "condition-unknown",
in: &security_beta.AuthorizationPolicy{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
Rules: []*security_beta.Rule{
{
When: []*security_beta.Condition{
{
Key: "key1",
Values: []string{"v1"},
},
},
},
},
},
valid: false,
},
}
for _, c := range cases {
if _, got := ValidateAuthorizationPolicy(config.Config{
Meta: config.Meta{
Name: "name",
Namespace: "namespace",
Annotations: c.annotations,
},
Spec: c.in,
}); (got == nil) != c.valid {
t.Errorf("got: %v\nwant: %v", got, c.valid)
}
}
}
func TestValidateSidecar(t *testing.T) {
tests := []struct {
name string
in *networking.Sidecar
valid bool
warn bool
}{
{"empty ingress and egress", &networking.Sidecar{}, false, false},
{"default", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, true, false},
{"import local namespace with wildcard", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"./*"},
},
},
}, true, false},
{"import local namespace with fqdn", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"./foo.com"},
},
},
}, true, false},
{"import nothing", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"~/*"},
},
},
}, true, false},
{"bad egress host 1", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*"},
},
},
}, false, false},
{"bad egress host 2", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"/"},
},
},
}, false, false},
{"empty egress host", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{},
},
},
}, false, false},
{"multiple wildcard egress", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{
"*/foo.com",
},
},
{
Hosts: []string{
"ns1/bar.com",
},
},
},
}, false, false},
{"wildcard egress not in end", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{
"*/foo.com",
},
},
{
Port: &networking.Port{
Protocol: "http",
Number: 8080,
Name: "h8080",
},
Hosts: []string{
"ns1/bar.com",
},
},
},
}, false, false},
{"invalid Port", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http1",
Number: 1000000,
Name: "",
},
Hosts: []string{
"ns1/bar.com",
},
},
},
}, false, false},
{"Port without name", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 8080,
},
Hosts: []string{
"ns1/bar.com",
},
},
},
}, true, false},
{"UDS bind in outbound", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Hosts: []string{
"ns1/bar.com",
},
Bind: "unix:///@foo/bar/com",
},
},
}, true, false},
{"UDS bind in inbound", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Bind: "unix:///@foo/bar/com",
DefaultEndpoint: "127.0.0.1:9999",
},
},
}, false, false},
{"UDS bind in outbound 2", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Hosts: []string{
"ns1/bar.com",
},
Bind: "unix:///foo/bar/com",
},
},
}, true, false},
{"invalid bind", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Hosts: []string{
"ns1/bar.com",
},
Bind: "foobar:///@foo/bar/com",
},
},
}, false, false},
{"invalid capture mode with uds bind", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Hosts: []string{
"ns1/bar.com",
},
Bind: "unix:///@foo/bar/com",
CaptureMode: networking.CaptureMode_IPTABLES,
},
},
}, false, false},
{"duplicate UDS bind", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Hosts: []string{
"ns1/bar.com",
},
Bind: "unix:///@foo/bar/com",
},
{
Port: &networking.Port{
Protocol: "http",
Number: 0,
Name: "uds",
},
Hosts: []string{
"ns1/bar.com",
},
Bind: "unix:///@foo/bar/com",
},
},
}, false, false},
{"duplicate ports", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
Hosts: []string{
"ns1/bar.com",
},
},
{
Port: &networking.Port{
Protocol: "tcp",
Number: 90,
Name: "tcp",
},
Hosts: []string{
"ns2/bar.com",
},
},
},
}, false, false},
{"ingress without port", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
DefaultEndpoint: "127.0.0.1:110",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"ingress with duplicate ports", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:110",
},
{
Port: &networking.Port{
Protocol: "tcp",
Number: 90,
Name: "bar",
},
DefaultEndpoint: "127.0.0.1:110",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"ingress without default endpoint", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, true, false},
{"ingress with invalid default endpoint IP", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "1.1.1.1:90",
},
},
}, false, false},
{"ingress with invalid default endpoint uds", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "unix:///",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"ingress with invalid default endpoint port", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:hi",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"valid ingress and egress", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, true, false},
{"valid ingress and empty egress", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
},
},
}, true, false},
{"empty", &networking.Sidecar{}, false, false},
{"just outbound traffic policy", &networking.Sidecar{OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
}}, true, false},
{"empty protocol", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, true, false},
{"ALLOW_ANY sidecar egress policy with no egress proxy ", &networking.Sidecar{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, true, false},
{"sidecar egress proxy with RESGISTRY_ONLY(default)", &networking.Sidecar{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
EgressProxy: &networking.Destination{
Host: "foo.bar",
Subset: "shiny",
Port: &networking.PortSelector{
Number: 5000,
},
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"sidecar egress proxy with ALLOW_ANY", &networking.Sidecar{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
EgressProxy: &networking.Destination{
Host: "foo.bar",
Subset: "shiny",
Port: &networking.PortSelector{
Number: 5000,
},
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, true, false},
{"sidecar egress proxy with ALLOW_ANY, service hostname invalid fqdn", &networking.Sidecar{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
EgressProxy: &networking.Destination{
Host: "foobar*123",
Subset: "shiny",
Port: &networking.PortSelector{
Number: 5000,
},
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"sidecar egress proxy(without Port) with ALLOW_ANY", &networking.Sidecar{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
EgressProxy: &networking.Destination{
Host: "foo.bar",
Subset: "shiny",
},
},
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"*/*"},
},
},
}, false, false},
{"sidecar egress only one wildcarded", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{
"*/*",
"test/a.com",
},
},
},
}, true, true},
{"sidecar egress wildcarded ns", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{
"*/b.com",
"test/a.com",
},
},
},
}, true, false},
{"sidecar egress duplicated with wildcarded same namespace", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{
"test/*",
"test/a.com",
},
},
},
}, true, true},
{"sidecar egress duplicated with wildcarded same namespace .", &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{
"./*",
"bar/a.com",
},
},
},
}, true, true},
{"ingress tls mode set to ISTIO_MUTUAL", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_ISTIO_MUTUAL,
},
},
},
}, false, false},
{"ingress tls mode set to ISTIO_AUTO_PASSTHROUGH", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "http",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_AUTO_PASSTHROUGH,
},
},
},
}, false, false},
{"ingress tls invalid protocol", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "tcp",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
},
},
},
}, false, false},
{"ingress tls httpRedirect is not supported", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "tcp",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
HttpsRedirect: true,
},
},
},
}, false, false},
{"ingress tls SAN entries are not supported", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "tcp",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
SubjectAltNames: []string{"httpbin.com"},
},
},
},
}, false, false},
{"ingress tls credentialName is not supported", &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Protocol: "tcp",
Number: 90,
Name: "foo",
},
DefaultEndpoint: "127.0.0.1:9999",
Tls: &networking.ServerTLSSettings{
Mode: networking.ServerTLSSettings_SIMPLE,
CredentialName: "secret-name",
},
},
},
}, false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warn, err := ValidateSidecar(config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
Spec: tt.in,
})
checkValidation(t, warn, err, tt.valid, tt.warn)
})
}
}
func TestValidateLocalityLbSetting(t *testing.T) {
cases := []struct {
name string
in *networking.LocalityLoadBalancerSetting
valid bool
}{
{
name: "valid mesh config without LocalityLoadBalancerSetting",
in: nil,
valid: true,
},
{
name: "invalid LocalityLoadBalancerSetting_Distribute total weight > 100",
in: &networking.LocalityLoadBalancerSetting{
Distribute: []*networking.LocalityLoadBalancerSetting_Distribute{
{
From: "a/b/c",
To: map[string]uint32{
"a/b/c": 80,
"a/b1": 25,
},
},
},
},
valid: false,
},
{
name: "invalid LocalityLoadBalancerSetting_Distribute total weight < 100",
in: &networking.LocalityLoadBalancerSetting{
Distribute: []*networking.LocalityLoadBalancerSetting_Distribute{
{
From: "a/b/c",
To: map[string]uint32{
"a/b/c": 80,
"a/b1": 15,
},
},
},
},
valid: false,
},
{
name: "invalid LocalityLoadBalancerSetting_Distribute weight = 0",
in: &networking.LocalityLoadBalancerSetting{
Distribute: []*networking.LocalityLoadBalancerSetting_Distribute{
{
From: "a/b/c",
To: map[string]uint32{
"a/b/c": 0,
"a/b1": 100,
},
},
},
},
valid: false,
},
{
name: "invalid LocalityLoadBalancerSetting specify both distribute and failover",
in: &networking.LocalityLoadBalancerSetting{
Distribute: []*networking.LocalityLoadBalancerSetting_Distribute{
{
From: "a/b/c",
To: map[string]uint32{
"a/b/c": 80,
"a/b1": 20,
},
},
},
Failover: []*networking.LocalityLoadBalancerSetting_Failover{
{
From: "region1",
To: "region2",
},
},
},
valid: false,
},
{
name: "invalid failover src and dst have same region",
in: &networking.LocalityLoadBalancerSetting{
Failover: []*networking.LocalityLoadBalancerSetting_Failover{
{
From: "region1",
To: "region1",
},
},
},
valid: false,
},
{
name: "invalid failover src contain '*' wildcard",
in: &networking.LocalityLoadBalancerSetting{
Failover: []*networking.LocalityLoadBalancerSetting_Failover{
{
From: "*",
To: "region2",
},
},
},
valid: false,
},
{
name: "invalid failover dst contain '*' wildcard",
in: &networking.LocalityLoadBalancerSetting{
Failover: []*networking.LocalityLoadBalancerSetting_Failover{
{
From: "region1",
To: "*",
},
},
},
valid: false,
},
{
name: "invalid failover src contain '/' separator",
in: &networking.LocalityLoadBalancerSetting{
Failover: []*networking.LocalityLoadBalancerSetting_Failover{
{
From: "region1/zone1",
To: "region2",
},
},
},
valid: false,
},
{
name: "invalid failover dst contain '/' separator",
in: &networking.LocalityLoadBalancerSetting{
Failover: []*networking.LocalityLoadBalancerSetting_Failover{
{
From: "region1",
To: "region2/zone1",
},
},
},
valid: false,
},
}
for _, c := range cases {
if got := validateLocalityLbSetting(c.in); (got == nil) != c.valid {
t.Errorf("ValidateLocalityLbSetting failed on %v: got valid=%v but wanted valid=%v: %v",
c.name, got == nil, c.valid, got)
}
}
}
func TestValidateLocalities(t *testing.T) {
cases := []struct {
name string
localities []string
valid bool
}{
{
name: "multi wildcard locality",
localities: []string{"*/zone/*"},
valid: false,
},
{
name: "wildcard not in suffix",
localities: []string{"*/zone"},
valid: false,
},
{
name: "explicit wildcard region overlap",
localities: []string{"*", "a/b/c"},
valid: false,
},
{
name: "implicit wildcard region overlap",
localities: []string{"a", "a/b/c"},
valid: false,
},
{
name: "explicit wildcard zone overlap",
localities: []string{"a/*", "a/b/c"},
valid: false,
},
{
name: "implicit wildcard zone overlap",
localities: []string{"a/b", "a/b/c"},
valid: false,
},
{
name: "explicit wildcard subzone overlap",
localities: []string{"a/b/*", "a/b/c"},
valid: false,
},
{
name: "implicit wildcard subzone overlap",
localities: []string{"a/b", "a/b/c"},
valid: false,
},
{
name: "valid localities",
localities: []string{"a1/*", "a2/*", "a3/b3/c3", "a4/b4", "a5/b5/*"},
valid: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := validateLocalities(c.localities)
if !c.valid && err == nil {
t.Errorf("expect invalid localities")
}
if c.valid && err != nil {
t.Errorf("expect valid localities. but got err %v", err)
}
})
}
}
func TestValidationIPAddress(t *testing.T) {
tests := []struct {
name string
addr string
ok bool
}{
{
name: "valid ipv4 address",
addr: "1.1.1.1",
ok: true,
},
{
name: "invalid ipv4 address",
addr: "1.1.A.1",
ok: false,
},
{
name: "valid ipv6 subnet",
addr: "2001:1::1",
ok: true,
},
{
name: "invalid ipv6 address",
addr: "2001:1:G::1",
ok: false,
},
}
for _, tt := range tests {
if err := ValidateIPAddress(tt.addr); err != nil {
if tt.ok {
t.Errorf("test: \"%s\" expected to succeed but failed with error: %+v", tt.name, err)
}
} else {
if !tt.ok {
t.Errorf("test: \"%s\" expected to fail but succeeded", tt.name)
}
}
}
}
func TestValidationIPSubnet(t *testing.T) {
tests := []struct {
name string
subnet string
ok bool
}{
{
name: "valid ipv4 subnet",
subnet: "1.1.1.1/24",
ok: true,
},
{
name: "invalid ipv4 subnet",
subnet: "1.1.1.1/48",
ok: false,
},
{
name: "valid ipv6 subnet",
subnet: "2001:1::1/64",
ok: true,
},
{
name: "invalid ipv6 subnet",
subnet: "2001:1::1/132",
ok: false,
},
}
for _, tt := range tests {
if err := ValidateIPSubnet(tt.subnet); err != nil {
if tt.ok {
t.Errorf("test: \"%s\" expected to succeed but failed with error: %+v", tt.name, err)
}
} else {
if !tt.ok {
t.Errorf("test: \"%s\" expected to fail but succeeded", tt.name)
}
}
}
}
func TestValidateRequestAuthentication(t *testing.T) {
cases := []struct {
name string
configName string
annotations map[string]string
in proto.Message
valid bool
}{
{
name: "empty spec",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{},
valid: true,
},
{
name: "another empty spec",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
Selector: &api.WorkloadSelector{},
},
valid: true,
},
{
name: "empty spec with non default name",
configName: someName,
in: &security_beta.RequestAuthentication{},
valid: true,
},
{
name: "dry run annotation not supported",
configName: someName,
annotations: map[string]string{"istio.io/dry-run": "true"},
in: &security_beta.RequestAuthentication{},
valid: false,
},
{
name: "default name with non empty selector",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
},
valid: true,
},
{
name: "empty jwt rule",
configName: "foo",
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{},
},
},
valid: false,
},
{
name: "empty issuer",
configName: "foo",
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "",
},
},
},
valid: false,
},
{
name: "bad JwksUri - no protocol",
configName: "foo",
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "foo.com",
},
},
},
valid: false,
},
{
name: "bad JwksUri - invalid port",
configName: "foo",
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "https://foo.com:not-a-number",
},
},
},
valid: false,
},
{
name: "empty value",
configName: "foo",
in: &security_beta.RequestAuthentication{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
"version": "",
},
},
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "https://foo.com/cert",
},
},
},
valid: true,
},
{
name: "bad selector - empty key",
configName: "foo",
in: &security_beta.RequestAuthentication{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
"": "v1",
},
},
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "https://foo.com/cert",
},
},
},
valid: false,
},
{
name: "bad header location",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "https://foo.com",
FromHeaders: []*security_beta.JWTHeader{
{
Name: "",
Prefix: "Bearer ",
},
},
},
},
},
valid: false,
},
{
name: "bad param location",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "https://foo.com",
FromParams: []string{""},
},
},
},
valid: false,
},
{
name: "good",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
JwksUri: "https://foo.com",
FromHeaders: []*security_beta.JWTHeader{
{
Name: "x-foo",
Prefix: "Bearer ",
},
},
},
},
},
valid: true,
},
{
name: "jwks ok",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
Jwks: "{ \"keys\":[ {\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\",\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}", // nolint: lll
FromHeaders: []*security_beta.JWTHeader{
{
Name: "x-foo",
Prefix: "Bearer ",
},
},
},
},
},
valid: true,
},
{
name: "jwks error",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.RequestAuthentication{
JwtRules: []*security_beta.JWTRule{
{
Issuer: "foo.com",
Jwks: "foo",
FromHeaders: []*security_beta.JWTHeader{
{
Name: "x-foo",
Prefix: "Bearer ",
},
},
},
},
},
valid: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if _, got := ValidateRequestAuthentication(config.Config{
Meta: config.Meta{
Name: c.configName,
Namespace: someNamespace,
Annotations: c.annotations,
},
Spec: c.in,
}); (got == nil) != c.valid {
t.Errorf("got(%v) != want(%v)\n", got, c.valid)
}
})
}
}
func TestValidatePeerAuthentication(t *testing.T) {
cases := []struct {
name string
configName string
in proto.Message
valid bool
}{
{
name: "empty spec",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.PeerAuthentication{},
valid: true,
},
{
name: "empty mtls",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.PeerAuthentication{
Selector: &api.WorkloadSelector{},
},
valid: true,
},
{
name: "empty spec with non default name",
configName: someName,
in: &security_beta.PeerAuthentication{},
valid: true,
},
{
name: "default name with non empty selector",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.PeerAuthentication{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
},
valid: true,
},
{
name: "empty port level mtls",
configName: "foo",
in: &security_beta.PeerAuthentication{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
PortLevelMtls: map[uint32]*security_beta.PeerAuthentication_MutualTLS{},
},
valid: false,
},
{
name: "empty selector with port level mtls",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.PeerAuthentication{
PortLevelMtls: map[uint32]*security_beta.PeerAuthentication_MutualTLS{
8080: {
Mode: security_beta.PeerAuthentication_MutualTLS_UNSET,
},
},
},
valid: false,
},
{
name: "port 0",
configName: "foo",
in: &security_beta.PeerAuthentication{
PortLevelMtls: map[uint32]*security_beta.PeerAuthentication_MutualTLS{
0: {
Mode: security_beta.PeerAuthentication_MutualTLS_UNSET,
},
},
},
valid: false,
},
{
name: "unset mode",
configName: constants.DefaultAuthenticationPolicyName,
in: &security_beta.PeerAuthentication{
Mtls: &security_beta.PeerAuthentication_MutualTLS{
Mode: security_beta.PeerAuthentication_MutualTLS_UNSET,
},
},
valid: true,
},
{
name: "port level",
configName: "port-level",
in: &security_beta.PeerAuthentication{
Selector: &api.WorkloadSelector{
MatchLabels: map[string]string{
"app": "httpbin",
},
},
PortLevelMtls: map[uint32]*security_beta.PeerAuthentication_MutualTLS{
8080: {
Mode: security_beta.PeerAuthentication_MutualTLS_UNSET,
},
},
},
valid: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if _, got := ValidatePeerAuthentication(config.Config{
Meta: config.Meta{
Name: c.configName,
Namespace: someNamespace,
},
Spec: c.in,
}); (got == nil) != c.valid {
t.Errorf("got(%v) != want(%v)\n", got, c.valid)
}
})
}
}
func TestServiceSettings(t *testing.T) {
cases := []struct {
name string
hosts []string
valid bool
}{
{
name: "good",
hosts: []string{
"*.foo.bar",
"bar.baz.svc.cluster.local",
},
valid: true,
},
{
name: "bad",
hosts: []string{
"foo.bar.*",
},
valid: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
m := meshconfig.MeshConfig{
ServiceSettings: []*meshconfig.MeshConfig_ServiceSettings{
{
Hosts: c.hosts,
},
},
}
if got := validateServiceSettings(&m); (got == nil) != c.valid {
t.Errorf("got(%v) != want(%v)\n", got, c.valid)
}
})
}
}
func TestValidateMeshNetworks(t *testing.T) {
testcases := []struct {
name string
mn *meshconfig.MeshNetworks
valid bool
}{
{
name: "Empty MeshNetworks",
mn: &meshconfig.MeshNetworks{},
valid: true,
},
{
name: "Valid MeshNetworks",
mn: &meshconfig.MeshNetworks{
Networks: map[string]*meshconfig.Network{
"n1": {
Endpoints: []*meshconfig.Network_NetworkEndpoints{
{
Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{
FromRegistry: "Kubernetes",
},
},
},
Gateways: []*meshconfig.Network_IstioNetworkGateway{
{
Gw: &meshconfig.Network_IstioNetworkGateway_RegistryServiceName{
RegistryServiceName: "istio-ingressgateway.dubbo-system.svc.cluster.local",
},
Port: 80,
},
},
},
"n2": {
Endpoints: []*meshconfig.Network_NetworkEndpoints{
{
Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{
FromRegistry: "cluster1",
},
},
},
Gateways: []*meshconfig.Network_IstioNetworkGateway{
{
Gw: &meshconfig.Network_IstioNetworkGateway_RegistryServiceName{
RegistryServiceName: "istio-ingressgateway.dubbo-system.svc.cluster.local",
},
Port: 443,
},
},
},
},
},
valid: true,
},
{
name: "Invalid Gateway Address",
mn: &meshconfig.MeshNetworks{
Networks: map[string]*meshconfig.Network{
"n1": {
Endpoints: []*meshconfig.Network_NetworkEndpoints{
{
Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{
FromRegistry: "Kubernetes",
},
},
},
Gateways: []*meshconfig.Network_IstioNetworkGateway{
{
Gw: &meshconfig.Network_IstioNetworkGateway_Address{
Address: "1nv@lidhostname",
},
Port: 80,
},
},
},
},
},
valid: false,
},
{
name: "Invalid registry name",
mn: &meshconfig.MeshNetworks{
Networks: map[string]*meshconfig.Network{
"n1": {
Endpoints: []*meshconfig.Network_NetworkEndpoints{
{
Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{
FromRegistry: "cluster.local",
},
},
},
Gateways: []*meshconfig.Network_IstioNetworkGateway{
{
Gw: &meshconfig.Network_IstioNetworkGateway_RegistryServiceName{
RegistryServiceName: "istio-ingressgateway.dubbo-system.svc.cluster.local",
},
Port: 80,
},
},
},
},
},
valid: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateMeshNetworks(tc.mn)
if err != nil && tc.valid {
t.Errorf("error not expected on valid meshnetworks: %v", tc.mn)
}
if err == nil && !tc.valid {
t.Errorf("expected an error on invalid meshnetworks: %v", tc.mn)
}
})
}
}
func Test_validateExportTo(t *testing.T) {
tests := []struct {
name string
namespace string
exportTo []string
isDestinationRuleWithSelector bool
isServiceEntry bool
wantErr bool
}{
{
name: "empty exportTo is okay",
namespace: "ns5",
wantErr: false,
},
{
name: "* is allowed",
namespace: "ns5",
exportTo: []string{"*"},
wantErr: false,
},
{
name: ". and ns1 are allowed",
namespace: "ns5",
exportTo: []string{".", "ns1"},
wantErr: false,
},
{
name: "bunch of namespaces in exportTo is okay",
namespace: "ns5",
exportTo: []string{"ns1", "ns2", "ns5"},
wantErr: false,
},
{
name: "~ is allowed for service entry configs",
namespace: "ns5",
exportTo: []string{"~"},
isServiceEntry: true,
wantErr: false,
},
{
name: "~ not allowed for non service entry configs",
namespace: "ns5",
exportTo: []string{"~", "ns1"},
wantErr: true,
},
{
name: ". and * together are not allowed",
namespace: "ns5",
exportTo: []string{".", "*"},
wantErr: true,
},
{
name: "* and ns1 together are not allowed",
namespace: "ns5",
exportTo: []string{"*", "ns1"},
wantErr: true,
},
{
name: ". and same namespace in exportTo is not okay",
namespace: "ns5",
exportTo: []string{".", "ns5"},
wantErr: true,
},
{
name: "duplicate namespaces in exportTo is not okay",
namespace: "ns5",
exportTo: []string{"ns1", "ns2", "ns1"},
wantErr: true,
},
{
name: "duplicate none in service entry exportTo is not okay",
namespace: "ns5",
exportTo: []string{"~", "~", "ns1"},
isServiceEntry: true,
wantErr: true,
},
{
name: "invalid namespace names are not okay",
namespace: "ns5",
exportTo: []string{"ns1_"},
wantErr: true,
},
{
name: "none and other namespaces cannot be combined in service entry exportTo",
namespace: "ns5",
exportTo: []string{"~", "ns1"},
isServiceEntry: true,
wantErr: true,
},
{
name: "destination rule with workloadselector cannot have exportTo (*)",
namespace: "ns5",
exportTo: []string{"*"},
isServiceEntry: false,
isDestinationRuleWithSelector: true,
wantErr: true,
},
{
name: "destination rule with workloadselector can have only exportTo (.)",
namespace: "ns5",
exportTo: []string{"."},
isServiceEntry: false,
isDestinationRuleWithSelector: true,
wantErr: false,
},
{
name: "destination rule with workloadselector cannot have another ns in exportTo (.)",
namespace: "ns5",
exportTo: []string{"somens"},
isServiceEntry: false,
isDestinationRuleWithSelector: true,
wantErr: true,
},
{
name: "destination rule with workloadselector cannot have another ns in addition to own ns in exportTo (.)",
namespace: "ns5",
exportTo: []string{".", "somens"},
isServiceEntry: false,
isDestinationRuleWithSelector: true,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateExportTo(tt.namespace, tt.exportTo, tt.isServiceEntry, tt.isDestinationRuleWithSelector); (err != nil) != tt.wantErr {
t.Errorf("validateExportTo() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidateTelemetry(t *testing.T) {
tests := []struct {
name string
in proto.Message
err string
warning string
}{
{"empty", &telemetry.Telemetry{}, "", ""},
{"invalid message", &networking.Server{}, "cannot cast", ""},
{
"multiple providers",
&telemetry.Telemetry{
Tracing: []*telemetry.Tracing{{
Providers: []*telemetry.ProviderRef{
{Name: "a"},
{Name: "b"},
},
}},
},
"", "multiple providers",
},
{
"multiple tracers",
&telemetry.Telemetry{
Tracing: []*telemetry.Tracing{{}, {}},
},
"", "multiple tracing",
},
{
"bad randomSamplingPercentage",
&telemetry.Telemetry{
Tracing: []*telemetry.Tracing{{
RandomSamplingPercentage: &wrapperspb.DoubleValue{Value: 101},
}},
},
"randomSamplingPercentage", "",
},
{
"tracing with a good custom tag",
&telemetry.Telemetry{
Tracing: []*telemetry.Tracing{{
CustomTags: map[string]*telemetry.Tracing_CustomTag{
"clusterID": {
Type: &telemetry.Tracing_CustomTag_Environment{
Environment: &telemetry.Tracing_Environment{
Name: "FOO",
},
},
},
},
}},
},
"", "",
},
{
"tracing with a nil custom tag",
&telemetry.Telemetry{
Tracing: []*telemetry.Tracing{{
CustomTags: map[string]*telemetry.Tracing_CustomTag{
"clusterID": nil,
},
}},
},
"tag 'clusterID' may not have a nil value", "",
},
{
"bad metrics operation",
&telemetry.Telemetry{
Metrics: []*telemetry.Metrics{{
Overrides: []*telemetry.MetricsOverrides{
{
TagOverrides: map[string]*telemetry.MetricsOverrides_TagOverride{
"my-tag": {
Operation: telemetry.MetricsOverrides_TagOverride_UPSERT,
Value: "",
},
},
},
},
}},
},
"must be set set when operation is UPSERT", "",
},
{
"good metrics operation",
&telemetry.Telemetry{
Metrics: []*telemetry.Metrics{{
Overrides: []*telemetry.MetricsOverrides{
{
TagOverrides: map[string]*telemetry.MetricsOverrides_TagOverride{
"my-tag": {
Operation: telemetry.MetricsOverrides_TagOverride_UPSERT,
Value: "some-cel-expression",
},
},
},
},
}},
},
"", "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warn, err := ValidateTelemetry(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: tt.in,
})
checkValidationMessage(t, warn, err, tt.warning, tt.err)
})
}
}
func TestValidateProxyConfig(t *testing.T) {
tests := []struct {
name string
in proto.Message
out string
warning string
}{
{"empty", &networkingv1beta1.ProxyConfig{}, "", ""},
{name: "invalid concurrency", in: &networkingv1beta1.ProxyConfig{
Concurrency: &wrapperspb.Int32Value{Value: -1},
}, out: "concurrency must be greater than or equal to 0"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warn, err := ValidateProxyConfig(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: tt.in,
})
checkValidationMessage(t, warn, err, tt.warning, tt.out)
})
}
}
func TestValidateTelemetryFilter(t *testing.T) {
cases := []struct {
filter *telemetry.AccessLogging_Filter
valid bool
}{
{
filter: &telemetry.AccessLogging_Filter{
Expression: "response.code >= 400",
},
valid: true,
},
{
filter: &telemetry.AccessLogging_Filter{
Expression: "connection.mtls && request.url_path.contains('v1beta3')",
},
valid: true,
},
{
filter: &telemetry.AccessLogging_Filter{
// TODO: find a better way to verify this
// this should be an invalid expression
Expression: "response.code",
},
valid: true,
},
{
filter: &telemetry.AccessLogging_Filter{
Expression: ")++++",
},
valid: false,
},
}
for _, tc := range cases {
t.Run("", func(t *testing.T) {
err := validateTelemetryFilter(tc.filter)
errFound := err != nil
if tc.valid && errFound {
t.Errorf("validateTelemetryFilter(%v) produced unexpected error: %v", tc.filter, err)
}
if !tc.valid && !errFound {
t.Errorf("validateTelemetryFilter(%v) did not produce expected error", tc.filter)
}
})
}
}
func TestValidateWasmPlugin(t *testing.T) {
tests := []struct {
name string
in proto.Message
out string
warning string
}{
{"empty", &extensions.WasmPlugin{}, "url field needs to be set", ""},
{"invalid message", &networking.Server{}, "cannot cast", ""},
{
"wrong scheme",
&extensions.WasmPlugin{
Url: "ftp://test.com/test",
},
"unsupported scheme", "",
},
{
"valid http",
&extensions.WasmPlugin{
Url: "http://test.com/test",
},
"", "",
},
{
"valid http w/ sha",
&extensions.WasmPlugin{
Url: "http://test.com/test",
Sha256: "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
},
"", "",
},
{
"short sha",
&extensions.WasmPlugin{
Url: "http://test.com/test",
Sha256: "01ba47",
},
"sha256 field must be 64 characters long", "",
},
{
"invalid sha",
&extensions.WasmPlugin{
Url: "http://test.com/test",
Sha256: "test",
},
"sha256 field must be 64 characters long", "",
},
{
"invalid sha characters",
&extensions.WasmPlugin{
Url: "http://test.com/test",
Sha256: "01Ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
},
"sha256 field must match [a-f0-9]{64} pattern", "",
},
{
"valid oci",
&extensions.WasmPlugin{
Url: "oci://test.com/test",
},
"", "",
},
{
"valid oci no scheme",
&extensions.WasmPlugin{
Url: "test.com/test",
},
"", "",
},
{
"invalid vm config - invalid env name",
&extensions.WasmPlugin{
Url: "test.com/test",
VmConfig: &extensions.VmConfig{
Env: []*extensions.EnvVar{
{
Name: "",
ValueFrom: extensions.EnvValueSource_HOST,
},
},
},
},
"spec.vmConfig.env invalid", "",
},
{
"invalid vm config - duplicate env",
&extensions.WasmPlugin{
Url: "test.com/test",
VmConfig: &extensions.VmConfig{
Env: []*extensions.EnvVar{
{
Name: "ENV1",
Value: "VAL1",
},
{
Name: "ENV1",
Value: "VAL1",
},
},
},
},
"duplicate env", "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warn, err := ValidateWasmPlugin(config.Config{
Meta: config.Meta{
Name: someName,
Namespace: someNamespace,
},
Spec: tt.in,
})
checkValidationMessage(t, warn, err, tt.warning, tt.out)
})
}
}
func TestRecurseMissingTypedConfig(t *testing.T) {
good := &listener.Filter{
Name: wellknown.TCPProxy,
ConfigType: &listener.Filter_TypedConfig{TypedConfig: nil},
}
bad := &listener.Filter{
Name: wellknown.TCPProxy,
}
assert.Equal(t, recurseMissingTypedConfig(good.ProtoReflect()), []string{}, "typed config set")
assert.Equal(t, recurseMissingTypedConfig(bad.ProtoReflect()), []string{wellknown.TCPProxy}, "typed config not set")
}