blob: ff418e63a10bc18a746eeb89e691d2b8708b546b [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 xds
import (
"fmt"
"os"
"path"
"testing"
)
import (
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"istio.io/api/mesh/v1alpha1"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/test/xdstest"
"github.com/apache/dubbo-go-pixiu/pkg/cluster"
"github.com/apache/dubbo-go-pixiu/pkg/config/mesh"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/env"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/structpath"
)
type SidecarTestConfig struct {
ImportedNamespaces []string
Resolution string
IngressListener bool
}
var scopeConfig = `
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
name: sidecar
namespace: app
spec:
{{- if .IngressListener }}
ingress:
- port:
number: 9080
protocol: HTTP
name: custom-http
defaultEndpoint: unix:///var/run/someuds.sock
{{- end }}
egress:
- hosts:
{{ range $i, $ns := .ImportedNamespaces }}
- {{$ns}}
{{ end }}
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: app
namespace: app
spec:
hosts:
- app.com
ports:
- number: 80
name: http
protocol: HTTP
resolution: {{.Resolution}}
endpoints:
{{- if eq .Resolution "DNS" }}
- address: app.com
{{- else }}
- address: 1.1.1.1
{{- end }}
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: excluded
namespace: excluded
spec:
hosts:
- app.com
ports:
- number: 80
name: http
protocol: HTTP
resolution: {{.Resolution}}
endpoints:
{{- if eq .Resolution "DNS" }}
- address: excluded.com
{{- else }}
- address: 9.9.9.9
{{- end }}
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: included
namespace: included
spec:
hosts:
- app.com
ports:
- number: 80
name: http
protocol: HTTP
resolution: {{.Resolution}}
endpoints:
{{- if eq .Resolution "DNS" }}
- address: included.com
{{- else }}
- address: 2.2.2.2
{{- end }}
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: app-https
namespace: app
spec:
hosts:
- app.cluster.local
addresses:
- 5.5.5.5
ports:
- number: 443
name: https
protocol: HTTPS
resolution: {{.Resolution}}
endpoints:
{{- if eq .Resolution "DNS" }}
- address: app.com
{{- else }}
- address: 10.10.10.10
{{- end }}
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: excluded-https
namespace: excluded
spec:
hosts:
- app.cluster.local
addresses:
- 5.5.5.5
ports:
- number: 4431
name: https
protocol: HTTPS
resolution: {{.Resolution}}
endpoints:
{{- if eq .Resolution "DNS" }}
- address: app.com
{{- else }}
- address: 10.10.10.10
{{- end }}
`
// TestServiceScoping is a high level test ensuring the Sidecar scoping works correctly, especially when
// there are multiple hostnames that are in different namespaces.
func TestServiceScoping(t *testing.T) {
baseProxy := func() *model.Proxy {
return &model.Proxy{
Metadata: &model.NodeMetadata{},
ID: "app.app",
Type: model.SidecarProxy,
IPAddresses: []string{"1.1.1.1"},
ConfigNamespace: "app",
}
}
t.Run("STATIC", func(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{
ConfigString: scopeConfig,
ConfigTemplateInput: SidecarTestConfig{
ImportedNamespaces: []string{"./*", "included/*"},
Resolution: "STATIC",
},
})
proxy := s.SetupProxy(baseProxy())
endpoints := xdstest.ExtractLoadAssignments(s.Endpoints(proxy))
if !listEqualUnordered(endpoints["outbound|80||app.com"], []string{"1.1.1.1:80"}) {
t.Fatalf("expected 1.1.1.1, got %v", endpoints["outbound|80||app.com"])
}
assertListEqual(t, xdstest.ExtractListenerNames(s.Listeners(proxy)), []string{
"0.0.0.0_80",
"5.5.5.5_443",
"virtualInbound",
"virtualOutbound",
})
})
t.Run("Ingress Listener", func(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{
ConfigString: scopeConfig,
ConfigTemplateInput: SidecarTestConfig{
ImportedNamespaces: []string{"./*", "included/*"},
Resolution: "STATIC",
IngressListener: true,
},
})
p := baseProxy()
// Change the node's IP so that it does not match with any service entry
p.IPAddresses = []string{"100.100.100.100"}
proxy := s.SetupProxy(p)
endpoints := xdstest.ExtractClusterEndpoints(s.Clusters(proxy))
eps := endpoints["inbound|9080||"]
if !listEqualUnordered(eps, []string{"/var/run/someuds.sock"}) {
t.Fatalf("expected /var/run/someuds.sock, got %v", eps)
}
assertListEqual(t, xdstest.ExtractListenerNames(s.Listeners(proxy)), []string{
"0.0.0.0_80",
"5.5.5.5_443",
"virtualInbound",
"virtualOutbound",
})
})
t.Run("DNS", func(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{
ConfigString: scopeConfig,
ConfigTemplateInput: SidecarTestConfig{
ImportedNamespaces: []string{"./*", "included/*"},
Resolution: "DNS",
},
})
proxy := s.SetupProxy(baseProxy())
assertListEqual(t, xdstest.ExtractClusterEndpoints(s.Clusters(proxy))["outbound|80||app.com"], []string{"app.com:80"})
})
t.Run("DNS no self import", func(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{
ConfigString: scopeConfig,
ConfigTemplateInput: SidecarTestConfig{
ImportedNamespaces: []string{"included/*"},
Resolution: "DNS",
},
})
proxy := s.SetupProxy(baseProxy())
assertListEqual(t, xdstest.ExtractClusterEndpoints(s.Clusters(proxy))["outbound|80||app.com"], []string{"included.com:80"})
})
}
func TestSidecarListeners(t *testing.T) {
t.Run("empty", func(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{})
proxy := s.SetupProxy(&model.Proxy{
IPAddresses: []string{"10.2.0.1"},
ID: "app3.testns",
})
structpath.ForProto(xdstest.ToDiscoveryResponse(s.Listeners(proxy))).
Exists("{.resources[?(@.address.socketAddress.portValue==15001)]}").
Select("{.resources[?(@.address.socketAddress.portValue==15001)]}").
Equals("virtualOutbound", "{.name}").
Equals("0.0.0.0", "{.address.socketAddress.address}").
Equals(wellknown.TCPProxy, "{.filterChains[1].filters[0].name}").
Equals("PassthroughCluster", "{.filterChains[1].filters[0].typedConfig.cluster}").
Equals("PassthroughCluster", "{.filterChains[1].filters[0].typedConfig.statPrefix}").
Equals(true, "{.useOriginalDst}").
CheckOrFail(t)
})
t.Run("mongo", func(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{
ConfigString: mustReadFile(t, "./tests/testdata/config/se-example.yaml"),
})
proxy := s.SetupProxy(&model.Proxy{
IPAddresses: []string{"10.2.0.1"},
ID: "app3.testns",
})
structpath.ForProto(xdstest.ToDiscoveryResponse(s.Listeners(proxy))).
Exists("{.resources[?(@.address.socketAddress.portValue==27018)]}").
Select("{.resources[?(@.address.socketAddress.portValue==27018)]}").
Equals("0.0.0.0", "{.address.socketAddress.address}").
// Example doing a struct comparison, note the pain with oneofs....
Equals(&core.SocketAddress{
Address: "0.0.0.0",
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: uint32(27018),
},
}, "{.address.socketAddress}").
Select("{.filterChains[0].filters[0]}").
Equals("envoy.mongo_proxy", "{.name}").
Select("{.typedConfig}").
Exists("{.statPrefix}").
CheckOrFail(t)
})
}
func TestEgressProxy(t *testing.T) {
s := NewFakeDiscoveryServer(t, FakeOptions{
ConfigString: `
# Add a random endpoint, otherwise there will be no routes to check
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: pod
spec:
hosts:
- pod.pod.svc.cluster.local
ports:
- number: 80
name: http
protocol: HTTP
resolution: STATIC
location: MESH_INTERNAL
endpoints:
- address: 10.10.10.20
---
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
name: sidecar-with-egressproxy
namespace: app
spec:
outboundTrafficPolicy:
mode: ALLOW_ANY
egressProxy:
host: foo.bar
subset: shiny
port:
number: 5000
egress:
- hosts:
- "*/*"
`,
})
proxy := s.SetupProxy(&model.Proxy{
ConfigNamespace: "app",
})
listeners := s.Listeners(proxy)
assertListEqual(t, xdstest.ExtractListenerNames(listeners), []string{
"0.0.0.0_80",
"virtualInbound",
"virtualOutbound",
})
expectedEgressCluster := "outbound|5000|shiny|foo.bar"
found := false
for _, f := range xdstest.ExtractListener("virtualOutbound", listeners).FilterChains {
// We want to check the match all filter chain, as this is testing the fallback logic
if f.FilterChainMatch != nil {
continue
}
tcp := xdstest.ExtractTCPProxy(t, f)
if tcp.GetCluster() != expectedEgressCluster {
t.Fatalf("got unexpected fallback destination: %v, want %v", tcp.GetCluster(), expectedEgressCluster)
}
found = true
}
if !found {
t.Fatalf("failed to find tcp proxy")
}
found = false
routes := s.Routes(proxy)
for _, rc := range routes {
for _, vh := range rc.GetVirtualHosts() {
if vh.GetName() == "allow_any" {
for _, r := range vh.GetRoutes() {
if expectedEgressCluster == r.GetRoute().GetCluster() {
found = true
break
}
}
break
}
}
}
if !found {
t.Fatalf("failed to find expected fallthrough route")
}
}
func assertListEqual(t test.Failer, a, b []string) {
t.Helper()
if !listEqualUnordered(a, b) {
t.Fatalf("Expected list %v to be equal to %v", a, b)
}
}
func mustReadFile(t *testing.T, f string) string {
b, err := os.ReadFile(path.Join(env.IstioSrc, f))
if err != nil {
t.Fatalf("failed to read %v: %v", f, err)
}
return string(b)
}
func TestClusterLocal(t *testing.T) {
tests := map[string]struct {
fakeOpts FakeOptions
serviceCluster string
wantClusterLocal map[cluster.ID][]string
wantNonClusterLocal map[cluster.ID][]string
}{
// set up a k8s service in each cluster, with a pod in each cluster and a workloadentry in cluster-1
"k8s service with pod and workloadentry": {
fakeOpts: func() FakeOptions {
k8sObjects := map[cluster.ID]string{
"cluster-1": "",
"cluster-2": "",
}
i := 1
for range k8sObjects {
clusterID := fmt.Sprintf("cluster-%d", i)
k8sObjects[cluster.ID(clusterID)] = fmt.Sprintf(`
apiVersion: v1
kind: Service
metadata:
labels:
app: echo-app
name: echo-app
namespace: default
spec:
clusterIP: 1.2.3.4
selector:
app: echo-app
ports:
- name: grpc
port: 7070
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: echo-app
name: echo-app-%s
namespace: default
---
apiVersion: v1
kind: Endpoints
metadata:
name: echo-app
namespace: default
labels:
app: echo-app
subsets:
- addresses:
- ip: 10.0.0.%d
ports:
- name: grpc
port: 7070
`, clusterID, i)
i++
}
return FakeOptions{
DefaultClusterName: "cluster-1",
KubernetesObjectStringByCluster: k8sObjects,
ConfigString: `
apiVersion: networking.istio.io/v1alpha3
kind: WorkloadEntry
metadata:
name: echo-app
namespace: default
spec:
address: 10.1.1.1
labels:
app: echo-app
`,
}
}(),
serviceCluster: "outbound|7070||echo-app.default.svc.cluster.local",
wantClusterLocal: map[cluster.ID][]string{
"cluster-1": {"10.0.0.1:7070", "10.1.1.1:7070"},
"cluster-2": {"10.0.0.2:7070"},
},
wantNonClusterLocal: map[cluster.ID][]string{
"cluster-1": {"10.0.0.1:7070", "10.1.1.1:7070", "10.0.0.2:7070"},
"cluster-2": {"10.0.0.1:7070", "10.1.1.1:7070", "10.0.0.2:7070"},
},
},
"serviceentry": {
fakeOpts: FakeOptions{
ConfigString: `
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: external-svc-mongocluster
spec:
hosts:
- mymongodb.somedomain
addresses:
- 192.192.192.192/24 # VIPs
ports:
- number: 27018
name: mongodb
protocol: MONGO
location: MESH_INTERNAL
resolution: STATIC
endpoints:
- address: 2.2.2.2
- address: 3.3.3.3
`,
},
serviceCluster: "outbound|27018||mymongodb.somedomain",
wantClusterLocal: map[cluster.ID][]string{
"Kubernetes": {"2.2.2.2:27018", "3.3.3.3:27018"},
"other": {},
},
wantNonClusterLocal: map[cluster.ID][]string{
"Kubernetes": {"2.2.2.2:27018", "3.3.3.3:27018"},
"other": {"2.2.2.2:27018", "3.3.3.3:27018"},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
for _, local := range []bool{true, false} {
name := "cluster-local"
want := tt.wantClusterLocal
if !local {
name = "non-cluster-local"
want = tt.wantNonClusterLocal
}
t.Run(name, func(t *testing.T) {
meshConfig := mesh.DefaultMeshConfig()
meshConfig.ServiceSettings = []*v1alpha1.MeshConfig_ServiceSettings{
{Hosts: []string{"*"}, Settings: &v1alpha1.MeshConfig_ServiceSettings_Settings{
ClusterLocal: local,
}},
}
fakeOpts := tt.fakeOpts
fakeOpts.MeshConfig = meshConfig
s := NewFakeDiscoveryServer(t, fakeOpts)
for clusterID := range want {
p := s.SetupProxy(&model.Proxy{Metadata: &model.NodeMetadata{ClusterID: clusterID}})
eps := xdstest.ExtractLoadAssignments(s.Endpoints(p))[tt.serviceCluster]
if want := want[clusterID]; !listEqualUnordered(eps, want) {
t.Errorf("got %v but want %v for %s", eps, want, clusterID)
}
}
})
}
})
}
}