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  }