1// Copyright 2019 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package pilot
16
17import (
18	"fmt"
19	"math"
20	"strings"
21	"testing"
22	"time"
23
24	"istio.io/istio/pkg/test/util/retry"
25
26	"istio.io/istio/pkg/test/framework/resource/environment"
27
28	"istio.io/istio/pkg/config/protocol"
29	"istio.io/istio/pkg/test/framework"
30	"istio.io/istio/pkg/test/framework/components/echo"
31	"istio.io/istio/pkg/test/framework/components/echo/echoboot"
32	"istio.io/istio/pkg/test/framework/components/namespace"
33	"istio.io/istio/pkg/test/util/file"
34	"istio.io/istio/pkg/test/util/tmpl"
35)
36
37//	Virtual service topology
38//
39//						 a
40//						|-------|
41//						| Host0 |
42//						|-------|
43//							|
44//							|
45//							|
46//		-------------------------
47//		|weight1	|weight2	|weight3
48//		|b			|c			|d
49//	|-------|	|-------|	|-------|
50//	| Host0 |	| Host1	|	| Host2 |
51//	|-------|	|-------|	|-------|
52//
53//
54
55const (
56	// Error threshold. For example, we expect 25% traffic, traffic distribution within [15%, 35%] is accepted.
57	errorThreshold = 10.0
58)
59
60type VirtualServiceConfig struct {
61	Name      string
62	Host0     string
63	Host1     string
64	Host2     string
65	Namespace string
66	Weight0   int32
67	Weight1   int32
68	Weight2   int32
69}
70
71func TestTrafficShifting(t *testing.T) {
72	// Traffic distribution
73	weights := map[string][]int32{
74		"20-80":    {20, 80},
75		"50-50":    {50, 50},
76		"33-33-34": {33, 33, 34},
77	}
78
79	framework.
80		NewTest(t).
81		RequiresEnvironment(environment.Kube).
82		Run(func(ctx framework.TestContext) {
83			ns := namespace.NewOrFail(t, ctx, namespace.Config{
84				Prefix: "traffic-shifting",
85				Inject: true,
86			})
87
88			var instances [4]echo.Instance
89			echoboot.NewBuilderOrFail(t, ctx).
90				With(&instances[0], echoConfig(ns, "a")).
91				With(&instances[1], echoConfig(ns, "b")).
92				With(&instances[2], echoConfig(ns, "c")).
93				With(&instances[3], echoConfig(ns, "d")).
94				BuildOrFail(t)
95
96			hosts := []string{"b", "c", "d"}
97
98			for k, v := range weights {
99				t.Run(k, func(t *testing.T) {
100					v = append(v, make([]int32, 3-len(v))...)
101
102					vsc := VirtualServiceConfig{
103						"traffic-shifting-rule",
104						hosts[0],
105						hosts[1],
106						hosts[2],
107						ns.Name(),
108						v[0],
109						v[1],
110						v[2],
111					}
112
113					deployment := tmpl.EvaluateOrFail(t, file.AsStringOrFail(t, "testdata/traffic-shifting.yaml"), vsc)
114					g.ApplyConfigOrFail(t, ns, deployment)
115
116					sendTraffic(t, 100, instances[0], instances[1], hosts, v, errorThreshold)
117				})
118			}
119		})
120}
121
122func echoConfig(ns namespace.Instance, name string) echo.Config {
123	return echo.Config{
124		Service:   name,
125		Namespace: ns,
126		Ports: []echo.Port{
127			{
128				Name:     "http",
129				Protocol: protocol.HTTP,
130				// We use a port > 1024 to not require root
131				InstancePort: 8090,
132			},
133		},
134		Subsets: []echo.SubsetConfig{{}},
135		Galley:  g,
136		Pilot:   p,
137	}
138}
139
140func sendTraffic(t *testing.T, batchSize int, from, to echo.Instance, hosts []string, weight []int32, errorThreshold float64) {
141	t.Helper()
142	// Send `batchSize` requests and ensure they are distributed as expected.
143	retry.UntilSuccessOrFail(t, func() error {
144		resp, err := from.Call(echo.CallOptions{
145			Target:   to,
146			PortName: "http",
147			Count:    batchSize,
148		})
149		if err != nil {
150			return fmt.Errorf("error during call: %v", err)
151		}
152		var totalRequests int
153		hitCount := map[string]int{}
154		for _, r := range resp {
155			for _, h := range hosts {
156				if strings.HasPrefix(r.Hostname, h+"-") {
157					hitCount[h]++
158					totalRequests++
159					break
160				}
161			}
162		}
163
164		for i, v := range hosts {
165			percentOfTrafficToHost := float64(hitCount[v]) * 100.0 / float64(totalRequests)
166			deltaFromExpected := math.Abs(float64(weight[i]) - percentOfTrafficToHost)
167			if errorThreshold-deltaFromExpected < 0 {
168				return fmt.Errorf("unexpected traffic weight for host %v. Expected %d%%, got %g%% (thresold: %g%%)",
169					v, weight[i], percentOfTrafficToHost, errorThreshold)
170			}
171			t.Logf("Got expected traffic weight for host %v. Expected %d%%, got %g%% (thresold: %g%%)",
172				v, weight[i], percentOfTrafficToHost, errorThreshold)
173		}
174		return nil
175	}, retry.Delay(time.Second))
176}
177