istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/locality_test.go (about) 1 //go:build integ 2 // +build integ 3 4 // Copyright Istio Authors 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 18 package pilot 19 20 import ( 21 "fmt" 22 "net/http" 23 "strings" 24 "testing" 25 26 "istio.io/istio/pkg/test/framework" 27 "istio.io/istio/pkg/test/framework/components/echo" 28 "istio.io/istio/pkg/test/scopes" 29 "istio.io/istio/tests/integration/pilot/common" 30 ) 31 32 const localityTemplate = ` 33 apiVersion: networking.istio.io/v1alpha3 34 kind: ServiceEntry 35 metadata: 36 name: external-service-locality 37 spec: 38 hosts: 39 - {{.Host}} 40 location: MESH_EXTERNAL 41 ports: 42 - name: http 43 number: 80 44 protocol: HTTP 45 resolution: {{.Resolution}} 46 endpoints: 47 - address: {{.Local}} 48 locality: region/zone/subzone 49 - address: {{.Remote}} 50 locality: notregion/notzone/notsubzone 51 {{ if ne .NearLocal "" }} 52 - address: {{.NearLocal}} 53 locality: "nearregion/zone/subzone" 54 {{ end }} 55 --- 56 apiVersion: networking.istio.io/v1alpha3 57 kind: DestinationRule 58 metadata: 59 name: external-service-locality 60 spec: 61 host: {{.Host}} 62 trafficPolicy: 63 connectionPool: 64 tcp: 65 connectTimeout: 250ms 66 loadBalancer: 67 simple: ROUND_ROBIN 68 localityLbSetting: 69 {{.LocalitySetting | indent 8 }} 70 outlierDetection: 71 interval: 1s 72 baseEjectionTime: 10m 73 maxEjectionPercent: 100` 74 75 type LocalityInput struct { 76 LocalitySetting string 77 Host string 78 Resolution string 79 Local string 80 NearLocal string 81 Remote string 82 } 83 84 const localityFailover = ` 85 failover: 86 - from: region 87 to: nearregion` 88 89 const failoverPriority = ` 90 failoverPriority: 91 - "topology.kubernetes.io/region" 92 - "topology.kubernetes.io/zone" 93 - "topology.istio.io/subzone"` 94 95 const localityDistribute = ` 96 distribute: 97 - from: region 98 to: 99 nearregion: 80 100 region: 20` 101 102 func TestLocality(t *testing.T) { 103 // nolint: staticcheck 104 framework. 105 NewTest(t). 106 RequiresSingleCluster(). 107 Run(func(t framework.TestContext) { 108 destA := apps.B[0] 109 destB := apps.C[0] 110 destC := apps.Naked[0] 111 if !t.Settings().Skip(echo.VM) { 112 // TODO do we even need this to be a VM 113 destC = apps.VM[0] 114 } 115 116 cases := []struct { 117 name string 118 input LocalityInput 119 expected map[string]int 120 }{ 121 { 122 "Prioritized/CDS", 123 LocalityInput{ 124 LocalitySetting: localityFailover, 125 Resolution: "DNS", 126 Local: destA.Config().Service, 127 Remote: destB.Config().Service, 128 }, 129 expectAllTrafficTo(destA.Config().Service), 130 }, 131 { 132 "Prioritized/EDS", 133 LocalityInput{ 134 LocalitySetting: localityFailover, 135 Resolution: "STATIC", 136 Local: destB.Address(), 137 Remote: destA.Address(), 138 }, 139 expectAllTrafficTo(destB.Config().Service), 140 }, 141 { 142 "Failover/CDS", 143 LocalityInput{ 144 LocalitySetting: localityFailover, 145 Resolution: "DNS", 146 Local: "fake-should-fail.example.com", 147 NearLocal: destA.Config().Service, 148 Remote: destB.Config().Service, 149 }, 150 expectAllTrafficTo(destA.Config().Service), 151 }, 152 { 153 "Failover/EDS", 154 LocalityInput{ 155 LocalitySetting: localityFailover, 156 Resolution: "STATIC", 157 Local: "10.10.10.10", 158 NearLocal: destB.Address(), 159 Remote: destA.Address(), 160 }, 161 expectAllTrafficTo(destB.Config().Service), 162 }, 163 { 164 "FailoverPriority/EDS", 165 LocalityInput{ 166 LocalitySetting: failoverPriority, 167 Resolution: "STATIC", 168 Local: destA.Address(), 169 NearLocal: destB.Address(), 170 Remote: destC.Address(), 171 }, 172 expectAllTrafficTo(destA.Config().Service), 173 }, 174 { 175 "Distribute/CDS", 176 LocalityInput{ 177 LocalitySetting: localityDistribute, 178 Resolution: "DNS", 179 Local: destB.Config().Service, 180 NearLocal: destA.Config().Service, 181 Remote: "fake-should-fail.example.com", 182 }, 183 map[string]int{ 184 destA.Config().Service: sendCount * .8, 185 destB.Config().Service: sendCount * .2, 186 }, 187 }, 188 { 189 "Distribute/EDS", 190 LocalityInput{ 191 LocalitySetting: localityDistribute, 192 Resolution: "STATIC", 193 Local: destA.Address(), 194 NearLocal: destB.Address(), 195 Remote: "10.10.10.10", 196 }, 197 map[string]int{ 198 destB.Config().Service: sendCount * .8, 199 destA.Config().Service: sendCount * .2, 200 }, 201 }, 202 } 203 for _, tt := range cases { 204 t.NewSubTest(tt.name).Run(func(t framework.TestContext) { 205 hostname := fmt.Sprintf("%s-fake-locality.example.com", strings.ToLower(strings.ReplaceAll(tt.name, "/", "-"))) 206 tt.input.Host = hostname 207 t.ConfigIstio(). 208 Eval(apps.Namespace.Name(), tt.input, localityTemplate). 209 ApplyOrFail(t) 210 sendTrafficOrFail(t, apps.A[0], hostname, tt.expected) 211 }) 212 } 213 }) 214 } 215 216 const sendCount = 50 217 218 func expectAllTrafficTo(dest string) map[string]int { 219 return map[string]int{dest: sendCount} 220 } 221 222 func sendTrafficOrFail(t framework.TestContext, from echo.Instance, host string, expected map[string]int) { 223 t.Helper() 224 headers := http.Header{} 225 headers.Add("Host", host) 226 checker := func(result echo.CallResult, inErr error) error { 227 if inErr != nil { 228 return inErr 229 } 230 got := map[string]int{} 231 for _, r := range result.Responses { 232 // Hostname will take form of svc-v1-random. We want to extract just 'svc' 233 parts := strings.SplitN(r.Hostname, "-", 2) 234 if len(parts) < 2 { 235 return fmt.Errorf("unexpected hostname: %v", r) 236 } 237 gotHost := parts[0] 238 got[gotHost]++ 239 } 240 scopes.Framework.Infof("Got responses: %+v", got) 241 for svc, reqs := range got { 242 expect := expected[svc] 243 if !common.AlmostEquals(reqs, expect, 3) { 244 return fmt.Errorf("unexpected request distribution. Expected: %+v, got: %+v", expected, got) 245 } 246 } 247 return nil 248 } 249 // This is a hack to remain infrastructure agnostic when running these tests 250 // We actually call the host set above not the endpoint we pass 251 _ = from.CallOrFail(t, echo.CallOptions{ 252 To: from, 253 Port: echo.Port{ 254 Name: "http", 255 }, 256 HTTP: echo.HTTP{ 257 Headers: headers, 258 }, 259 Count: sendCount, 260 Check: checker, 261 }) 262 }