google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/xdsresource/unmarshal_eds_test.go (about)

     1  /*
     2   *
     3   * Copyright 2021 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package xdsresource
    19  
    20  import (
    21  	"fmt"
    22  	"net"
    23  	"strconv"
    24  	"testing"
    25  
    26  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    27  	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    28  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    29  	v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/google/go-cmp/cmp/cmpopts"
    32  	"google.golang.org/grpc/internal/envconfig"
    33  	"google.golang.org/grpc/internal/pretty"
    34  	"google.golang.org/grpc/internal/testutils"
    35  	"google.golang.org/grpc/xds/internal"
    36  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version"
    37  	"google.golang.org/protobuf/proto"
    38  	"google.golang.org/protobuf/types/known/anypb"
    39  	"google.golang.org/protobuf/types/known/structpb"
    40  	"google.golang.org/protobuf/types/known/wrapperspb"
    41  )
    42  
    43  func (s) TestEDSParseRespProto(t *testing.T) {
    44  	tests := []struct {
    45  		name    string
    46  		m       *v3endpointpb.ClusterLoadAssignment
    47  		want    EndpointsUpdate
    48  		wantErr bool
    49  	}{
    50  		{
    51  			name: "missing-priority",
    52  			m: func() *v3endpointpb.ClusterLoadAssignment {
    53  				clab0 := newClaBuilder("test", nil)
    54  				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
    55  				clab0.addLocality("locality-2", 1, 2, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
    56  				return clab0.Build()
    57  			}(),
    58  			want:    EndpointsUpdate{},
    59  			wantErr: true,
    60  		},
    61  		{
    62  			name: "missing-locality-ID",
    63  			m: func() *v3endpointpb.ClusterLoadAssignment {
    64  				clab0 := newClaBuilder("test", nil)
    65  				clab0.addLocality("", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
    66  				return clab0.Build()
    67  			}(),
    68  			want:    EndpointsUpdate{},
    69  			wantErr: true,
    70  		},
    71  		{
    72  			name: "zero-endpoint-weight",
    73  			m: func() *v3endpointpb.ClusterLoadAssignment {
    74  				clab0 := newClaBuilder("test", nil)
    75  				clab0.addLocality("locality-0", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, &addLocalityOptions{Weight: []uint32{0}})
    76  				return clab0.Build()
    77  			}(),
    78  			want:    EndpointsUpdate{},
    79  			wantErr: true,
    80  		},
    81  		{
    82  			name: "duplicate-locality-in-the-same-priority",
    83  			m: func() *v3endpointpb.ClusterLoadAssignment {
    84  				clab0 := newClaBuilder("test", nil)
    85  				clab0.addLocality("locality-0", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
    86  				clab0.addLocality("locality-0", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil) // Duplicate locality with the same priority.
    87  				return clab0.Build()
    88  			}(),
    89  			want:    EndpointsUpdate{},
    90  			wantErr: true,
    91  		},
    92  		{
    93  			name: "missing locality weight",
    94  			m: func() *v3endpointpb.ClusterLoadAssignment {
    95  				clab0 := newClaBuilder("test", nil)
    96  				clab0.addLocality("locality-1", 0, 1, []endpointOpts{{addrWithPort: "addr1:314"}}, &addLocalityOptions{
    97  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_HEALTHY},
    98  				})
    99  				clab0.addLocality("locality-2", 0, 0, []endpointOpts{{addrWithPort: "addr2:159"}}, &addLocalityOptions{
   100  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_HEALTHY},
   101  				})
   102  				return clab0.Build()
   103  			}(),
   104  			want: EndpointsUpdate{},
   105  		},
   106  		{
   107  			name: "max sum of weights at the same priority exceeded",
   108  			m: func() *v3endpointpb.ClusterLoadAssignment {
   109  				clab0 := newClaBuilder("test", nil)
   110  				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
   111  				clab0.addLocality("locality-2", 4294967295, 1, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
   112  				clab0.addLocality("locality-3", 1, 1, []endpointOpts{{addrWithPort: "addr2:88"}}, nil)
   113  				return clab0.Build()
   114  			}(),
   115  			want:    EndpointsUpdate{},
   116  			wantErr: true,
   117  		},
   118  		{
   119  			name: "duplicate endpoint address",
   120  			m: func() *v3endpointpb.ClusterLoadAssignment {
   121  				clab0 := newClaBuilder("test", nil)
   122  				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr:997"}}, nil)
   123  				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr:997"}}, nil)
   124  				return clab0.Build()
   125  			}(),
   126  			want:    EndpointsUpdate{},
   127  			wantErr: true,
   128  		},
   129  		{
   130  			name: "good",
   131  			m: func() *v3endpointpb.ClusterLoadAssignment {
   132  				clab0 := newClaBuilder("test", nil)
   133  				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr1:314"}}, &addLocalityOptions{
   134  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
   135  					Weight: []uint32{271},
   136  				})
   137  				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr2:159"}}, &addLocalityOptions{
   138  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
   139  					Weight: []uint32{828},
   140  				})
   141  				return clab0.Build()
   142  			}(),
   143  			want: EndpointsUpdate{
   144  				Drops: nil,
   145  				Localities: []Locality{
   146  					{
   147  						Endpoints: []Endpoint{{
   148  							Addresses:    []string{"addr1:314"},
   149  							HealthStatus: EndpointHealthStatusUnhealthy,
   150  							Weight:       271,
   151  						}},
   152  						ID:       internal.LocalityID{SubZone: "locality-1"},
   153  						Priority: 1,
   154  						Weight:   1,
   155  					},
   156  					{
   157  						Endpoints: []Endpoint{{
   158  							Addresses:    []string{"addr2:159"},
   159  							HealthStatus: EndpointHealthStatusDraining,
   160  							Weight:       828,
   161  						}},
   162  						ID:       internal.LocalityID{SubZone: "locality-2"},
   163  						Priority: 0,
   164  						Weight:   1,
   165  					},
   166  				},
   167  			},
   168  			wantErr: false,
   169  		},
   170  		{
   171  			name: "good duplicate locality with different priority",
   172  			m: func() *v3endpointpb.ClusterLoadAssignment {
   173  				clab0 := newClaBuilder("test", nil)
   174  				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr1:314"}}, &addLocalityOptions{
   175  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
   176  					Weight: []uint32{271},
   177  				})
   178  				// Same locality name, but with different priority.
   179  				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr2:159"}}, &addLocalityOptions{
   180  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
   181  					Weight: []uint32{828},
   182  				})
   183  				return clab0.Build()
   184  			}(),
   185  			want: EndpointsUpdate{
   186  				Drops: nil,
   187  				Localities: []Locality{
   188  					{
   189  						Endpoints: []Endpoint{{
   190  							Addresses:    []string{"addr1:314"},
   191  							HealthStatus: EndpointHealthStatusUnhealthy,
   192  							Weight:       271,
   193  						}},
   194  						ID:       internal.LocalityID{SubZone: "locality-1"},
   195  						Priority: 1,
   196  						Weight:   1,
   197  					},
   198  					{
   199  						Endpoints: []Endpoint{{
   200  							Addresses:    []string{"addr2:159"},
   201  							HealthStatus: EndpointHealthStatusDraining,
   202  							Weight:       828,
   203  						}},
   204  						ID:       internal.LocalityID{SubZone: "locality-1"},
   205  						Priority: 0,
   206  						Weight:   1,
   207  					},
   208  				},
   209  			},
   210  			wantErr: false,
   211  		},
   212  	}
   213  	for _, tt := range tests {
   214  		t.Run(tt.name, func(t *testing.T) {
   215  			got, err := parseEDSRespProto(tt.m)
   216  			if (err != nil) != tt.wantErr {
   217  				t.Errorf("parseEDSRespProto() error = %v, wantErr %v", err, tt.wantErr)
   218  				return
   219  			}
   220  			if d := cmp.Diff(got, tt.want, cmpopts.EquateEmpty()); d != "" {
   221  				t.Errorf("parseEDSRespProto() got = %v, want %v, diff: %v", got, tt.want, d)
   222  			}
   223  		})
   224  	}
   225  }
   226  
   227  func (s) TestEDSParseRespProtoAdditionalAddrs(t *testing.T) {
   228  	origDualstackEndpointsEnabled := envconfig.XDSDualstackEndpointsEnabled
   229  	defer func() {
   230  		envconfig.XDSDualstackEndpointsEnabled = origDualstackEndpointsEnabled
   231  	}()
   232  	envconfig.XDSDualstackEndpointsEnabled = true
   233  
   234  	tests := []struct {
   235  		name    string
   236  		m       *v3endpointpb.ClusterLoadAssignment
   237  		want    EndpointsUpdate
   238  		wantErr bool
   239  	}{
   240  		{
   241  			name: "duplicate primary address in self additional addresses",
   242  			m: func() *v3endpointpb.ClusterLoadAssignment {
   243  				clab0 := newClaBuilder("test", nil)
   244  				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:998"}}}, nil)
   245  				return clab0.Build()
   246  			}(),
   247  			want:    EndpointsUpdate{},
   248  			wantErr: true,
   249  		},
   250  		{
   251  			name: "duplicate primary address in other locality additional addresses",
   252  			m: func() *v3endpointpb.ClusterLoadAssignment {
   253  				clab0 := newClaBuilder("test", nil)
   254  				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr:997"}}, nil)
   255  				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:997"}}}, nil)
   256  				return clab0.Build()
   257  			}(),
   258  			want:    EndpointsUpdate{},
   259  			wantErr: true,
   260  		},
   261  		{
   262  			name: "duplicate additional address in self additional addresses",
   263  			m: func() *v3endpointpb.ClusterLoadAssignment {
   264  				clab0 := newClaBuilder("test", nil)
   265  				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:999", "addr:999"}}}, nil)
   266  				return clab0.Build()
   267  			}(),
   268  			want:    EndpointsUpdate{},
   269  			wantErr: true,
   270  		},
   271  		{
   272  			name: "duplicate additional address in other locality additional addresses",
   273  			m: func() *v3endpointpb.ClusterLoadAssignment {
   274  				clab0 := newClaBuilder("test", nil)
   275  				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr:997", additionalAddrWithPorts: []string{"addr:1000"}}}, nil)
   276  				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:1000"}}}, nil)
   277  				return clab0.Build()
   278  			}(),
   279  			want:    EndpointsUpdate{},
   280  			wantErr: true,
   281  		},
   282  		{
   283  			name: "multiple localities",
   284  			m: func() *v3endpointpb.ClusterLoadAssignment {
   285  				clab0 := newClaBuilder("test", nil)
   286  				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr1:997", additionalAddrWithPorts: []string{"addr1:1000"}}}, &addLocalityOptions{
   287  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
   288  					Weight: []uint32{271},
   289  				})
   290  				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr2:998", additionalAddrWithPorts: []string{"addr2:1000"}}}, &addLocalityOptions{
   291  					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_HEALTHY},
   292  					Weight: []uint32{828},
   293  				})
   294  				return clab0.Build()
   295  			}(),
   296  			want: EndpointsUpdate{
   297  				Drops: nil,
   298  				Localities: []Locality{
   299  					{
   300  						Endpoints: []Endpoint{{
   301  							Addresses:    []string{"addr1:997", "addr1:1000"},
   302  							HealthStatus: EndpointHealthStatusUnhealthy,
   303  							Weight:       271,
   304  						}},
   305  						ID:       internal.LocalityID{SubZone: "locality-1"},
   306  						Priority: 1,
   307  						Weight:   1,
   308  					},
   309  					{
   310  						Endpoints: []Endpoint{{
   311  							Addresses:    []string{"addr2:998", "addr2:1000"},
   312  							HealthStatus: EndpointHealthStatusHealthy,
   313  							Weight:       828,
   314  						}},
   315  						ID:       internal.LocalityID{SubZone: "locality-2"},
   316  						Priority: 0,
   317  						Weight:   1,
   318  					},
   319  				},
   320  			},
   321  		},
   322  	}
   323  
   324  	for _, tt := range tests {
   325  		t.Run(tt.name, func(t *testing.T) {
   326  			got, err := parseEDSRespProto(tt.m)
   327  			if (err != nil) != tt.wantErr {
   328  				t.Errorf("parseEDSRespProto() error = %v, wantErr %v", err, tt.wantErr)
   329  				return
   330  			}
   331  			if d := cmp.Diff(got, tt.want, cmpopts.EquateEmpty()); d != "" {
   332  				t.Errorf("parseEDSRespProto() got = %v, want %v, diff: %v", got, tt.want, d)
   333  			}
   334  		})
   335  	}
   336  }
   337  
   338  func (s) TestUnmarshalEndpointHashKey(t *testing.T) {
   339  	baseCLA := &v3endpointpb.ClusterLoadAssignment{
   340  		Endpoints: []*v3endpointpb.LocalityLbEndpoints{
   341  			{
   342  				Locality: &v3corepb.Locality{Region: "r"},
   343  				LbEndpoints: []*v3endpointpb.LbEndpoint{
   344  					{
   345  						HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
   346  							Endpoint: &v3endpointpb.Endpoint{
   347  								Address: &v3corepb.Address{
   348  									Address: &v3corepb.Address_SocketAddress{
   349  										SocketAddress: &v3corepb.SocketAddress{
   350  											Address: "test-address",
   351  											PortSpecifier: &v3corepb.SocketAddress_PortValue{
   352  												PortValue: 8080,
   353  											},
   354  										},
   355  									},
   356  								},
   357  							},
   358  						},
   359  					},
   360  				},
   361  				LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1},
   362  			},
   363  		},
   364  	}
   365  
   366  	tests := []struct {
   367  		name         string
   368  		metadata     *v3corepb.Metadata
   369  		wantHashKey  string
   370  		compatEnvVar bool
   371  	}{
   372  		{
   373  			name:        "no metadata",
   374  			metadata:    nil,
   375  			wantHashKey: "",
   376  		},
   377  		{
   378  			name:        "empty metadata",
   379  			metadata:    &v3corepb.Metadata{},
   380  			wantHashKey: "",
   381  		},
   382  		{
   383  			name: "filter metadata without envoy.lb",
   384  			metadata: &v3corepb.Metadata{
   385  				FilterMetadata: map[string]*structpb.Struct{
   386  					"test-filter": {},
   387  				},
   388  			},
   389  			wantHashKey: "",
   390  		},
   391  		{
   392  			name: "nil envoy.lb",
   393  			metadata: &v3corepb.Metadata{
   394  				FilterMetadata: map[string]*structpb.Struct{
   395  					"envoy.lb": nil,
   396  				},
   397  			},
   398  			wantHashKey: "",
   399  		},
   400  		{
   401  			name: "envoy.lb without hash key",
   402  			metadata: &v3corepb.Metadata{
   403  				FilterMetadata: map[string]*structpb.Struct{
   404  					"envoy.lb": {
   405  						Fields: map[string]*structpb.Value{
   406  							"hash_key": {
   407  								Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
   408  							},
   409  						},
   410  					},
   411  				},
   412  			},
   413  			wantHashKey: "",
   414  		},
   415  		{
   416  			name: "envoy.lb with hash key, compat mode off",
   417  			metadata: &v3corepb.Metadata{
   418  				FilterMetadata: map[string]*structpb.Struct{
   419  					"envoy.lb": {
   420  						Fields: map[string]*structpb.Value{
   421  							"hash_key": {
   422  								Kind: &structpb.Value_StringValue{StringValue: "test-hash-key"},
   423  							},
   424  						},
   425  					},
   426  				},
   427  			},
   428  			wantHashKey: "test-hash-key",
   429  		},
   430  		{
   431  			name: "envoy.lb with hash key, compat mode on",
   432  			metadata: &v3corepb.Metadata{
   433  				FilterMetadata: map[string]*structpb.Struct{
   434  					"envoy.lb": {
   435  						Fields: map[string]*structpb.Value{
   436  							"hash_key": {
   437  								Kind: &structpb.Value_StringValue{StringValue: "test-hash-key"},
   438  							},
   439  						},
   440  					},
   441  				},
   442  			},
   443  			wantHashKey:  "",
   444  			compatEnvVar: true,
   445  		},
   446  	}
   447  
   448  	for _, test := range tests {
   449  		t.Run(test.name, func(t *testing.T) {
   450  			testutils.SetEnvConfig(t, &envconfig.XDSEndpointHashKeyBackwardCompat, test.compatEnvVar)
   451  
   452  			cla := proto.Clone(baseCLA).(*v3endpointpb.ClusterLoadAssignment)
   453  			cla.Endpoints[0].LbEndpoints[0].Metadata = test.metadata
   454  			marshalledCLA := testutils.MarshalAny(t, cla)
   455  			_, update, err := unmarshalEndpointsResource(marshalledCLA)
   456  			if err != nil {
   457  				t.Fatalf("unmarshalEndpointsResource() got error = %v, want success", err)
   458  			}
   459  			got := update.Localities[0].Endpoints[0].HashKey
   460  			if got != test.wantHashKey {
   461  				t.Errorf("unmarshalEndpointResource() endpoint hash key: got %s, want %s", got, test.wantHashKey)
   462  			}
   463  		})
   464  	}
   465  }
   466  
   467  func (s) TestUnmarshalEndpoints(t *testing.T) {
   468  	var v3EndpointsAny = testutils.MarshalAny(t, func() *v3endpointpb.ClusterLoadAssignment {
   469  		clab0 := newClaBuilder("test", nil)
   470  		clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr1:314"}}, &addLocalityOptions{
   471  			Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
   472  			Weight: []uint32{271},
   473  		})
   474  		clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr2:159"}}, &addLocalityOptions{
   475  			Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
   476  			Weight: []uint32{828},
   477  		})
   478  		return clab0.Build()
   479  	}())
   480  
   481  	tests := []struct {
   482  		name       string
   483  		resource   *anypb.Any
   484  		wantName   string
   485  		wantUpdate EndpointsUpdate
   486  		wantErr    bool
   487  	}{
   488  		{
   489  			name:     "non-clusterLoadAssignment resource type",
   490  			resource: &anypb.Any{TypeUrl: version.V3HTTPConnManagerURL},
   491  			wantErr:  true,
   492  		},
   493  		{
   494  			name: "badly marshaled clusterLoadAssignment resource",
   495  			resource: &anypb.Any{
   496  				TypeUrl: version.V3EndpointsURL,
   497  				Value:   []byte{1, 2, 3, 4},
   498  			},
   499  			wantErr: true,
   500  		},
   501  		{
   502  			name: "bad endpoints resource",
   503  			resource: testutils.MarshalAny(t, func() *v3endpointpb.ClusterLoadAssignment {
   504  				clab0 := newClaBuilder("test", nil)
   505  				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
   506  				clab0.addLocality("locality-2", 1, 2, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
   507  				return clab0.Build()
   508  			}()),
   509  			wantName: "test",
   510  			wantErr:  true,
   511  		},
   512  		{
   513  			name:     "v3 endpoints",
   514  			resource: v3EndpointsAny,
   515  			wantName: "test",
   516  			wantUpdate: EndpointsUpdate{
   517  				Drops: nil,
   518  				Localities: []Locality{
   519  					{
   520  						Endpoints: []Endpoint{{
   521  							Addresses:    []string{"addr1:314"},
   522  							HealthStatus: EndpointHealthStatusUnhealthy,
   523  							Weight:       271,
   524  						}},
   525  						ID:       internal.LocalityID{SubZone: "locality-1"},
   526  						Priority: 1,
   527  						Weight:   1,
   528  					},
   529  					{
   530  						Endpoints: []Endpoint{{
   531  							Addresses:    []string{"addr2:159"},
   532  							HealthStatus: EndpointHealthStatusDraining,
   533  							Weight:       828,
   534  						}},
   535  						ID:       internal.LocalityID{SubZone: "locality-2"},
   536  						Priority: 0,
   537  						Weight:   1,
   538  					},
   539  				},
   540  				Raw: v3EndpointsAny,
   541  			},
   542  		},
   543  		{
   544  			name:     "v3 endpoints wrapped",
   545  			resource: testutils.MarshalAny(t, &v3discoverypb.Resource{Resource: v3EndpointsAny}),
   546  			wantName: "test",
   547  			wantUpdate: EndpointsUpdate{
   548  				Drops: nil,
   549  				Localities: []Locality{
   550  					{
   551  						Endpoints: []Endpoint{{
   552  							Addresses:    []string{"addr1:314"},
   553  							HealthStatus: EndpointHealthStatusUnhealthy,
   554  							Weight:       271,
   555  						}},
   556  						ID:       internal.LocalityID{SubZone: "locality-1"},
   557  						Priority: 1,
   558  						Weight:   1,
   559  					},
   560  					{
   561  						Endpoints: []Endpoint{{
   562  							Addresses:    []string{"addr2:159"},
   563  							HealthStatus: EndpointHealthStatusDraining,
   564  							Weight:       828,
   565  						}},
   566  						ID:       internal.LocalityID{SubZone: "locality-2"},
   567  						Priority: 0,
   568  						Weight:   1,
   569  					},
   570  				},
   571  				Raw: v3EndpointsAny,
   572  			},
   573  		},
   574  	}
   575  	for _, test := range tests {
   576  		t.Run(test.name, func(t *testing.T) {
   577  			name, update, err := unmarshalEndpointsResource(test.resource)
   578  			if (err != nil) != test.wantErr {
   579  				t.Fatalf("unmarshalEndpointsResource(%s), got err: %v, wantErr: %v", pretty.ToJSON(test.resource), err, test.wantErr)
   580  			}
   581  			if name != test.wantName {
   582  				t.Errorf("unmarshalEndpointsResource(%s), got name: %s, want: %s", pretty.ToJSON(test.resource), name, test.wantName)
   583  			}
   584  			if diff := cmp.Diff(update, test.wantUpdate, cmpOpts); diff != "" {
   585  				t.Errorf("unmarshalEndpointsResource(%s), got unexpected update, diff (-got +want): %v", pretty.ToJSON(test.resource), diff)
   586  			}
   587  		})
   588  	}
   589  }
   590  
   591  // claBuilder builds a ClusterLoadAssignment, aka EDS
   592  // response.
   593  type claBuilder struct {
   594  	v *v3endpointpb.ClusterLoadAssignment
   595  }
   596  
   597  // newClaBuilder creates a claBuilder.
   598  func newClaBuilder(clusterName string, dropPercents []uint32) *claBuilder {
   599  	var drops []*v3endpointpb.ClusterLoadAssignment_Policy_DropOverload
   600  	for i, d := range dropPercents {
   601  		drops = append(drops, &v3endpointpb.ClusterLoadAssignment_Policy_DropOverload{
   602  			Category: fmt.Sprintf("test-drop-%d", i),
   603  			DropPercentage: &v3typepb.FractionalPercent{
   604  				Numerator:   d,
   605  				Denominator: v3typepb.FractionalPercent_HUNDRED,
   606  			},
   607  		})
   608  	}
   609  
   610  	return &claBuilder{
   611  		v: &v3endpointpb.ClusterLoadAssignment{
   612  			ClusterName: clusterName,
   613  			Policy: &v3endpointpb.ClusterLoadAssignment_Policy{
   614  				DropOverloads: drops,
   615  			},
   616  		},
   617  	}
   618  }
   619  
   620  // addLocalityOptions contains options when adding locality to the builder.
   621  type addLocalityOptions struct {
   622  	Health []v3corepb.HealthStatus
   623  	Weight []uint32
   624  }
   625  
   626  type endpointOpts struct {
   627  	addrWithPort            string
   628  	additionalAddrWithPorts []string
   629  }
   630  
   631  func addressFromStr(addrWithPort string) *v3corepb.Address {
   632  	host, portStr, err := net.SplitHostPort(addrWithPort)
   633  	if err != nil {
   634  		panic("failed to split " + addrWithPort)
   635  	}
   636  	port, err := strconv.Atoi(portStr)
   637  	if err != nil {
   638  		panic("failed to atoi " + portStr)
   639  	}
   640  
   641  	return &v3corepb.Address{
   642  		Address: &v3corepb.Address_SocketAddress{
   643  			SocketAddress: &v3corepb.SocketAddress{
   644  				Protocol:      v3corepb.SocketAddress_TCP,
   645  				Address:       host,
   646  				PortSpecifier: &v3corepb.SocketAddress_PortValue{PortValue: uint32(port)},
   647  			},
   648  		},
   649  	}
   650  }
   651  
   652  // addLocality adds a locality to the builder.
   653  func (clab *claBuilder) addLocality(subzone string, weight uint32, priority uint32, endpoints []endpointOpts, opts *addLocalityOptions) {
   654  	var lbEndPoints []*v3endpointpb.LbEndpoint
   655  	for i, e := range endpoints {
   656  		var additionalAddrs []*v3endpointpb.Endpoint_AdditionalAddress
   657  		for _, a := range e.additionalAddrWithPorts {
   658  			additionalAddrs = append(additionalAddrs, &v3endpointpb.Endpoint_AdditionalAddress{
   659  				Address: addressFromStr(a),
   660  			})
   661  		}
   662  		lbe := &v3endpointpb.LbEndpoint{
   663  			HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
   664  				Endpoint: &v3endpointpb.Endpoint{
   665  					Address:             addressFromStr(e.addrWithPort),
   666  					AdditionalAddresses: additionalAddrs,
   667  				},
   668  			},
   669  		}
   670  		if opts != nil {
   671  			if i < len(opts.Health) {
   672  				lbe.HealthStatus = opts.Health[i]
   673  			}
   674  			if i < len(opts.Weight) {
   675  				lbe.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: opts.Weight[i]}
   676  			}
   677  		}
   678  		lbEndPoints = append(lbEndPoints, lbe)
   679  	}
   680  
   681  	var localityID *v3corepb.Locality
   682  	if subzone != "" {
   683  		localityID = &v3corepb.Locality{
   684  			Region:  "",
   685  			Zone:    "",
   686  			SubZone: subzone,
   687  		}
   688  	}
   689  
   690  	clab.v.Endpoints = append(clab.v.Endpoints, &v3endpointpb.LocalityLbEndpoints{
   691  		Locality:            localityID,
   692  		LbEndpoints:         lbEndPoints,
   693  		LoadBalancingWeight: &wrapperspb.UInt32Value{Value: weight},
   694  		Priority:            priority,
   695  	})
   696  }
   697  
   698  // Build builds ClusterLoadAssignment.
   699  func (clab *claBuilder) Build() *v3endpointpb.ClusterLoadAssignment {
   700  	return clab.v
   701  }