istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/mirror_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 "math" 23 "strings" 24 "testing" 25 26 "github.com/hashicorp/go-multierror" 27 "k8s.io/apimachinery/pkg/util/rand" 28 29 "istio.io/istio/pkg/config/protocol" 30 "istio.io/istio/pkg/log" 31 "istio.io/istio/pkg/test/framework" 32 "istio.io/istio/pkg/test/framework/components/echo" 33 "istio.io/istio/pkg/test/framework/components/echo/common/deployment" 34 "istio.io/istio/pkg/test/util/retry" 35 ) 36 37 // Virtual service topology 38 // 39 // a b c 40 // |-------| |-------| mirror |-------| 41 // | Host0 | ----------> | Host1 | ----------> | Host2 | 42 // |-------| |-------| |-------| 43 // 44 45 type VirtualServiceMirrorConfig struct { 46 Name string 47 Absent bool 48 Percent float64 49 MirrorHost string 50 } 51 52 type testCaseMirror struct { 53 name string 54 absent bool 55 percentage float64 56 threshold float64 57 expectedDestination echo.Instances 58 } 59 60 type mirrorTestOptions struct { 61 cases []testCaseMirror 62 mirrorHost string 63 } 64 65 var mirrorProtocols = []protocol.Instance{protocol.HTTP, protocol.GRPC} 66 67 func TestMirroring(t *testing.T) { 68 runMirrorTest(t, mirrorTestOptions{ 69 cases: []testCaseMirror{ 70 { 71 name: "mirror-percent-absent", 72 absent: true, 73 percentage: 100.0, 74 threshold: 0.0, 75 }, 76 { 77 name: "mirror-50", 78 percentage: 50.0, 79 threshold: 10.0, 80 }, 81 { 82 name: "mirror-10", 83 percentage: 10.0, 84 threshold: 5.0, 85 }, 86 { 87 name: "mirror-0", 88 percentage: 0.0, 89 threshold: 0.0, 90 }, 91 }, 92 }) 93 } 94 95 // Tests mirroring to an external service. Uses same topology as the test above, a -> b -> external, with "external" being external. 96 // 97 // Since we don't want to rely on actual external websites, we simulate that by using a Sidecar to limit connectivity 98 // from "a" so that it cannot reach "external" directly, and we use a ServiceEntry to define our "external" website, which 99 // is static and points to the service "external" ip. 100 101 // Thus when "a" tries to mirror to the external service, it is actually connecting to "external" (which is not part of the 102 // mesh because of the Sidecar), then we can inspect "external" logs to verify the requests were properly mirrored. 103 func TestMirroringExternalService(t *testing.T) { 104 header := "" 105 if len(apps.External.All) > 0 { 106 header = apps.External.All.Config().HostHeader() 107 } 108 runMirrorTest(t, mirrorTestOptions{ 109 mirrorHost: header, 110 cases: []testCaseMirror{ 111 { 112 name: "mirror-external", 113 absent: true, 114 percentage: 100.0, 115 threshold: 0.0, 116 expectedDestination: apps.External.All, 117 }, 118 }, 119 }) 120 } 121 122 func runMirrorTest(t *testing.T, options mirrorTestOptions) { 123 framework. 124 NewTest(t). 125 Run(func(t framework.TestContext) { 126 for _, c := range options.cases { 127 t.NewSubTest(c.name).Run(func(t framework.TestContext) { 128 mirrorHost := options.mirrorHost 129 if len(mirrorHost) == 0 { 130 mirrorHost = deployment.CSvc 131 } 132 vsc := VirtualServiceMirrorConfig{ 133 c.name, 134 c.absent, 135 c.percentage, 136 mirrorHost, 137 } 138 139 // we only apply to config clusters 140 t.ConfigIstio().EvalFile(apps.Namespace.Name(), vsc, "testdata/traffic-mirroring-template.yaml"). 141 ApplyOrFail(t) 142 143 for _, podA := range apps.A { 144 podA := podA 145 t.NewSubTest(fmt.Sprintf("from %s", podA.Config().Cluster.StableName())).Run(func(t framework.TestContext) { 146 for _, proto := range mirrorProtocols { 147 t.NewSubTest(string(proto)).Run(func(t framework.TestContext) { 148 retry.UntilSuccessOrFail(t, func() error { 149 testID := rand.String(16) 150 if err := sendTrafficMirror(podA, apps.B, proto, testID); err != nil { 151 return err 152 } 153 expected := c.expectedDestination 154 if expected == nil { 155 expected = apps.C 156 } 157 158 return verifyTrafficMirror(apps.B, expected, c, testID) 159 }, echo.DefaultCallRetryOptions()...) 160 }) 161 } 162 }) 163 } 164 }) 165 } 166 }) 167 } 168 169 func sendTrafficMirror(from echo.Instance, to echo.Target, proto protocol.Instance, testID string) error { 170 options := echo.CallOptions{ 171 To: to, 172 Count: 100, 173 Port: echo.Port{ 174 Name: strings.ToLower(proto.String()), 175 }, 176 Retry: echo.Retry{ 177 NoRetry: true, 178 }, 179 } 180 switch proto { 181 case protocol.HTTP: 182 options.HTTP.Path = "/" + testID 183 case protocol.GRPC: 184 options.Message = testID 185 default: 186 return fmt.Errorf("protocol not supported in mirror testing: %s", proto) 187 } 188 189 _, err := from.Call(options) 190 if err != nil { 191 return err 192 } 193 194 return nil 195 } 196 197 func verifyTrafficMirror(dest, mirror echo.Instances, tc testCaseMirror, testID string) error { 198 countB, err := logCount(dest, testID) 199 if err != nil { 200 return err 201 } 202 203 countC, err := logCount(mirror, testID) 204 if err != nil { 205 return err 206 } 207 208 actualPercent := (countC / countB) * 100 209 deltaFromExpected := math.Abs(actualPercent - tc.percentage) 210 211 var merr *multierror.Error 212 if tc.threshold-deltaFromExpected < 0 { 213 err := fmt.Errorf("unexpected mirror traffic. Expected %g%%, got %.1f%% (threshold: %g%%, testID: %s)", 214 tc.percentage, actualPercent, tc.threshold, testID) 215 log.Infof("%v", err) 216 merr = multierror.Append(merr, err) 217 } else { 218 log.Infof("Got expected mirror traffic. Expected %g%%, got %.1f%% (threshold: %g%%, , testID: %s)", 219 tc.percentage, actualPercent, tc.threshold, testID) 220 } 221 222 return merr.ErrorOrNil() 223 } 224 225 func logCount(instances echo.Instances, testID string) (float64, error) { 226 counts := map[string]float64{} 227 for _, instance := range instances { 228 workloads, err := instance.Workloads() 229 if err != nil { 230 return -1, fmt.Errorf("failed to get Subsets: %v", err) 231 } 232 var logs string 233 for _, w := range workloads { 234 l, err := w.Logs() 235 if err != nil { 236 return -1, fmt.Errorf("failed getting logs: %v", err) 237 } 238 logs += l 239 } 240 if c := float64(strings.Count(logs, testID)); c > 0 { 241 counts[instance.Config().Cluster.Name()] = c 242 } 243 } 244 var total float64 245 for _, c := range counts { 246 total += c 247 } 248 // TODO(landow) mirorr split does not always hit all clusters 249 return total, nil 250 }