blob: b4d7ac748b3b780508666cafbda622dd198c9a88 [file] [log] [blame]
//go:build integ
// +build integ
// 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 pilot
import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"
"text/template"
)
import (
"github.com/Masterminds/sprig/v3"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo"
"github.com/apache/dubbo-go-pixiu/pkg/test/scopes"
"github.com/apache/dubbo-go-pixiu/tests/integration/pilot/common"
)
const localityTemplate = `
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: external-service-locality
spec:
hosts:
- {{.Host}}
location: MESH_EXTERNAL
ports:
- name: http
number: 80
protocol: HTTP
resolution: {{.Resolution}}
endpoints:
- address: {{.Local}}
locality: region/zone/subzone
- address: {{.Remote}}
locality: notregion/notzone/notsubzone
{{ if ne .NearLocal "" }}
- address: {{.NearLocal}}
locality: "nearregion/zone/subzone"
{{ end }}
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: external-service-locality
spec:
host: {{.Host}}
trafficPolicy:
connectionPool:
tcp:
connectTimeout: 250ms
loadBalancer:
simple: ROUND_ROBIN
localityLbSetting:
{{.LocalitySetting | indent 8 }}
outlierDetection:
interval: 1s
baseEjectionTime: 10m
maxEjectionPercent: 100`
type LocalityInput struct {
LocalitySetting string
Host string
Resolution string
Local string
NearLocal string
Remote string
}
const localityFailover = `
failover:
- from: region
to: nearregion`
const failoverPriority = `
failoverPriority:
- "topology.kubernetes.io/region"
- "topology.kubernetes.io/zone"
- "topology.istio.io/subzone"`
const localityDistribute = `
distribute:
- from: region
to:
nearregion: 80
region: 20`
func TestLocality(t *testing.T) {
framework.
NewTest(t).
Features("traffic.locality").
RequiresSingleCluster().
Run(func(t framework.TestContext) {
destA := apps.B[0]
destB := apps.C[0]
destC := apps.Naked[0]
if !t.Settings().Skip(echo.VM) {
// TODO do we even need this to be a VM
destC = apps.VM[0]
}
cases := []struct {
name string
input LocalityInput
expected map[string]int
}{
{
"Prioritized/CDS",
LocalityInput{
LocalitySetting: localityFailover,
Resolution: "DNS",
Local: destA.Config().Service,
Remote: destB.Config().Service,
},
expectAllTrafficTo(destA.Config().Service),
},
{
"Prioritized/EDS",
LocalityInput{
LocalitySetting: localityFailover,
Resolution: "STATIC",
Local: destB.Address(),
Remote: destA.Address(),
},
expectAllTrafficTo(destB.Config().Service),
},
{
"Failover/CDS",
LocalityInput{
LocalitySetting: localityFailover,
Resolution: "DNS",
Local: "fake-should-fail.example.com",
NearLocal: destA.Config().Service,
Remote: destB.Config().Service,
},
expectAllTrafficTo(destA.Config().Service),
},
{
"Failover/EDS",
LocalityInput{
LocalitySetting: localityFailover,
Resolution: "STATIC",
Local: "10.10.10.10",
NearLocal: destB.Address(),
Remote: destA.Address(),
},
expectAllTrafficTo(destB.Config().Service),
},
{
"FailoverPriority/EDS",
LocalityInput{
LocalitySetting: failoverPriority,
Resolution: "STATIC",
Local: destA.Address(),
NearLocal: destB.Address(),
Remote: destC.Address(),
},
expectAllTrafficTo(destA.Config().Service),
},
{
"Distribute/CDS",
LocalityInput{
LocalitySetting: localityDistribute,
Resolution: "DNS",
Local: destB.Config().Service,
NearLocal: destA.Config().Service,
Remote: "fake-should-fail.example.com",
},
map[string]int{
destA.Config().Service: sendCount * .8,
destB.Config().Service: sendCount * .2,
},
},
{
"Distribute/EDS",
LocalityInput{
LocalitySetting: localityDistribute,
Resolution: "STATIC",
Local: destA.Address(),
NearLocal: destB.Address(),
Remote: "10.10.10.10",
},
map[string]int{
destB.Config().Service: sendCount * .8,
destA.Config().Service: sendCount * .2,
},
},
}
for _, tt := range cases {
t.NewSubTest(tt.name).Run(func(t framework.TestContext) {
hostname := fmt.Sprintf("%s-fake-locality.example.com", strings.ToLower(strings.ReplaceAll(tt.name, "/", "-")))
tt.input.Host = hostname
t.ConfigIstio().YAML(apps.Namespace.Name(), runTemplate(t, localityTemplate, tt.input)).ApplyOrFail(t)
sendTrafficOrFail(t, apps.A[0], hostname, tt.expected)
})
}
})
}
const sendCount = 50
func expectAllTrafficTo(dest string) map[string]int {
return map[string]int{dest: sendCount}
}
func sendTrafficOrFail(t framework.TestContext, from echo.Instance, host string, expected map[string]int) {
t.Helper()
headers := http.Header{}
headers.Add("Host", host)
checker := func(result echo.CallResult, inErr error) error {
if inErr != nil {
return inErr
}
got := map[string]int{}
for _, r := range result.Responses {
// Hostname will take form of svc-v1-random. We want to extract just 'svc'
parts := strings.SplitN(r.Hostname, "-", 2)
if len(parts) < 2 {
return fmt.Errorf("unexpected hostname: %v", r)
}
gotHost := parts[0]
got[gotHost]++
}
scopes.Framework.Infof("Got responses: %+v", got)
for svc, reqs := range got {
expect := expected[svc]
if !common.AlmostEquals(reqs, expect, 3) {
return fmt.Errorf("unexpected request distribution. Expected: %+v, got: %+v", expected, got)
}
}
return nil
}
// This is a hack to remain infrastructure agnostic when running these tests
// We actually call the host set above not the endpoint we pass
_ = from.CallOrFail(t, echo.CallOptions{
To: from,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Headers: headers,
},
Count: sendCount,
Check: checker,
})
}
func runTemplate(t test.Failer, tmpl string, input interface{}) string {
tt, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(tmpl)
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
if err := tt.Execute(&buf, input); err != nil {
t.Fatal(err)
}
return buf.String()
}