diff --git a/xds/internal/clients/config.go b/xds/internal/clients/config.go index 5640a1e0afed..a2220f995737 100644 --- a/xds/internal/clients/config.go +++ b/xds/internal/clients/config.go @@ -16,11 +16,10 @@ * */ -// Package clients provides implementations of the xDS and LRS clients, -// enabling applications to communicate with xDS management servers and report -// load. +// Package clients provides implementations of the clients to interact with +// xDS and LRS servers. // -// xDS Client +// # xDS Client // // The xDS client allows applications to: // - Create client instances with in-memory configurations. @@ -41,100 +40,30 @@ // // NOTICE: This package is EXPERIMENTAL and may be changed or removed // in a later release. -// -// See [README](https://github.com/grpc/grpc-go/tree/master/xds/clients/README.md). package clients -import ( - "fmt" - "slices" - "strings" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" - - v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" -) - -// ServerConfig holds settings for connecting to an xDS management server. -type ServerConfig struct { - // ServerURI is the target URI of the xDS management server. +// ServerIdentifier holds identifying information for connecting to an xDS +// management or LRS server. +type ServerIdentifier struct { + // ServerURI is the target URI of the server. ServerURI string - // IgnoreResourceDeletion is a server feature which if set to true, - // indicates that resource deletion errors can be ignored and cached - // resource data can be used. - // - // This will be removed in the future once we implement gRFC A88 - // and two new fields FailOnDataErrors and - // ResourceTimerIsTransientError will be introduced. - IgnoreResourceDeletion bool - // Extensions can be populated with arbitrary data to be passed to the - // [TransportBuilder] and/or xDS Client's ResourceType implementations. + // TransportBuilder and/or xDS Client's ResourceType implementations. // This field can be used to provide additional configuration or context // specific to the user's needs. // // The xDS and LRS clients do not interpret the contents of this field. - // It is the responsibility of the user's custom [TransportBuilder] and/or + // It is the responsibility of the user's custom TransportBuilder and/or // ResourceType implementations to handle and interpret these extensions. // - // For example, a custom [TransportBuilder] might use this field to + // For example, a custom TransportBuilder might use this field to // configure a specific security credentials. - // - // Note: For custom types used in Extensions, ensure an Equal(any) bool - // method is implemented for equality checks on ServerConfig. Extensions any } -// equal returns true if sc and other are considered equal. -func (sc *ServerConfig) equal(other *ServerConfig) bool { - switch { - case sc == nil && other == nil: - return true - case (sc != nil) != (other != nil): - return false - case sc.ServerURI != other.ServerURI: - return false - case sc.IgnoreResourceDeletion != other.IgnoreResourceDeletion: - return false - } - if sc.Extensions == nil && other.Extensions == nil { - return true - } - if ex, ok := sc.Extensions.(interface{ Equal(any) bool }); ok && ex.Equal(other.Extensions) { - return true - } - return false -} - -// String returns a string representation of the [ServerConfig]. -// -// WARNING: This method is primarily intended for logging and testing -// purposes. The output returned by this method is not guaranteed to be stable -// and may change at any time. Do not rely on it for production use. -func (sc *ServerConfig) String() string { - return strings.Join([]string{sc.ServerURI, fmt.Sprintf("%v", sc.IgnoreResourceDeletion)}, "-") -} - -// Authority contains configuration for an xDS control plane authority. -type Authority struct { - // XDSServers contains the list of server configurations for this authority. - XDSServers []ServerConfig - - // Extensions can be populated with arbitrary data to be passed to the xDS - // Client's user specific implementations. This field can be used to - // provide additional configuration or context specific to the user's - // needs. - // - // The xDS and LRS clients do not interpret the contents of this field. It - // is the responsibility of the user's implementations to handle and - // interpret these extensions. - Extensions any -} - -// Node represents the identity of the xDS client, allowing -// management servers to identify the source of xDS requests. +// Node represents the identity of the xDS client, allowing xDS and LRS servers +// to identify the source of xDS requests. type Node struct { // ID is a string identifier of the application. ID string @@ -150,40 +79,6 @@ type Node struct { UserAgentName string // UserAgentVersion is the user agent version of application. UserAgentVersion string - // ClientFeatures is a list of xDS features supported by this client. - // These features are set within the xDS client, but may be overridden only - // for testing purposes. - clientFeatures []string -} - -// toProto converts an instance of [Node] to its protobuf representation. -func (n Node) toProto() *v3corepb.Node { - return &v3corepb.Node{ - Id: n.ID, - Cluster: n.Cluster, - Locality: func() *v3corepb.Locality { - if n.Locality.isEmpty() { - return nil - } - return &v3corepb.Locality{ - Region: n.Locality.Region, - Zone: n.Locality.Zone, - SubZone: n.Locality.SubZone, - } - }(), - Metadata: func() *structpb.Struct { - if n.Metadata == nil { - return nil - } - if md, ok := n.Metadata.(*structpb.Struct); ok { - return proto.Clone(md).(*structpb.Struct) - } - return nil - }(), - UserAgentName: n.UserAgentName, - UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: n.UserAgentVersion}, - ClientFeatures: slices.Clone(n.clientFeatures), - } } // Locality represents the location of the xDS client application. @@ -195,13 +90,3 @@ type Locality struct { // SubZone is the further subdivision within a zone. SubZone string } - -// isEmpty reports whether l is considered empty. -func (l Locality) isEmpty() bool { - return l.equal(Locality{}) -} - -// equal returns true if l and other are considered equal. -func (l Locality) equal(other Locality) bool { - return l.Region == other.Region && l.Zone == other.Zone && l.SubZone == other.SubZone -} diff --git a/xds/internal/clients/config_test.go b/xds/internal/clients/config_test.go deleted file mode 100644 index 48417db034d4..000000000000 --- a/xds/internal/clients/config_test.go +++ /dev/null @@ -1,353 +0,0 @@ -/* - * - * Copyright 2024 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package clients - -import ( - "testing" - - v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - "github.com/google/go-cmp/cmp" - "google.golang.org/grpc/internal/grpctest" - "google.golang.org/protobuf/testing/protocmp" - "google.golang.org/protobuf/types/known/structpb" -) - -type s struct { - grpctest.Tester -} - -func Test(t *testing.T) { - grpctest.RunSubTests(t, s{}) -} - -type testServerConfigExtension struct{ x int } - -func (ts testServerConfigExtension) Equal(other any) bool { - ots, ok := other.(testServerConfigExtension) - if !ok { - return false - } - return ts.x == ots.x -} - -func newStructProtoFromMap(t *testing.T, input map[string]any) *structpb.Struct { - t.Helper() - - ret, err := structpb.NewStruct(input) - if err != nil { - t.Fatalf("Failed to create new struct proto from map %v: %v", input, err) - } - return ret -} - -func (s) TestServerConfig_Equal(t *testing.T) { - tests := []struct { - name string - s1 *ServerConfig - s2 *ServerConfig - wantEq bool - }{ - { - name: "both_nil", - s1: nil, - s2: nil, - wantEq: true, - }, - { - name: "one_nil", - s1: nil, - s2: &ServerConfig{}, - wantEq: false, - }, - { - name: "other_nil", - s1: &ServerConfig{}, - s2: nil, - wantEq: false, - }, - { - name: "both_empty_and_equal", - s1: &ServerConfig{}, - s2: &ServerConfig{}, - wantEq: true, - }, - { - name: "different_ServerURI", - s1: &ServerConfig{ServerURI: "foo"}, - s2: &ServerConfig{ServerURI: "bar"}, - wantEq: false, - }, - { - name: "different_IgnoreResourceDeletion", - s1: &ServerConfig{IgnoreResourceDeletion: true}, - s2: &ServerConfig{}, - wantEq: false, - }, - { - name: "different_Extensions_with_no_Equal_method", - s1: &ServerConfig{ - Extensions: 1, - }, - s2: &ServerConfig{ - Extensions: 2, - }, - wantEq: false, // By default, if there's no Equal method, they are unequal - }, - { - name: "same_Extensions_with_no_Equal_method", - s1: &ServerConfig{ - Extensions: 1, - }, - s2: &ServerConfig{ - Extensions: 1, - }, - wantEq: false, // By default, if there's no Equal method, they are unequal - }, - { - name: "different_Extensions_with_Equal_method", - s1: &ServerConfig{ - Extensions: testServerConfigExtension{1}, - }, - s2: &ServerConfig{ - Extensions: testServerConfigExtension{2}, - }, - wantEq: false, - }, - { - name: "same_Extensions_same_with_Equal_method", - s1: &ServerConfig{ - Extensions: testServerConfigExtension{1}, - }, - s2: &ServerConfig{ - Extensions: testServerConfigExtension{1}, - }, - wantEq: true, - }, - { - name: "first_config_Extensions_is_nil", - s1: &ServerConfig{ - Extensions: testServerConfigExtension{1}, - }, - s2: &ServerConfig{ - Extensions: nil, - }, - wantEq: false, - }, - { - name: "other_config_Extensions_is_nil", - s1: &ServerConfig{ - Extensions: nil, - }, - s2: &ServerConfig{ - Extensions: testServerConfigExtension{2}, - }, - wantEq: false, - }, - { - name: "all_fields_same", - s1: &ServerConfig{ - ServerURI: "foo", - IgnoreResourceDeletion: true, - Extensions: testServerConfigExtension{1}, - }, - s2: &ServerConfig{ - ServerURI: "foo", - IgnoreResourceDeletion: true, - Extensions: testServerConfigExtension{1}, - }, - wantEq: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if gotEq := test.s1.equal(test.s2); gotEq != test.wantEq { - t.Errorf("Equal() = %v, want %v", gotEq, test.wantEq) - } - }) - } -} - -func (s) TestLocality_IsEmpty(t *testing.T) { - tests := []struct { - name string - locality Locality - want bool - }{ - { - name: "empty_locality", - locality: Locality{}, - want: true, - }, - { - name: "non_empty_region", - locality: Locality{Region: "region"}, - want: false, - }, - { - name: "non_empty_zone", - locality: Locality{Zone: "zone"}, - want: false, - }, - { - name: "non_empty_subzone", - locality: Locality{SubZone: "subzone"}, - want: false, - }, - { - name: "non_empty_all_fields", - locality: Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, - want: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if got := test.locality.isEmpty(); got != test.want { - t.Errorf("IsEmpty() = %v, want %v", got, test.want) - } - }) - } -} - -func (s) TestLocality_Equal(t *testing.T) { - tests := []struct { - name string - l1 Locality - l2 Locality - wantEq bool - }{ - { - name: "both_equal", - l1: Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, - l2: Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, - wantEq: true, - }, - { - name: "different_regions", - l1: Locality{Region: "region1", Zone: "zone", SubZone: "subzone"}, - l2: Locality{Region: "region2", Zone: "zone", SubZone: "subzone"}, - wantEq: false, - }, - - { - name: "different_zones", - l1: Locality{Region: "region", Zone: "zone1", SubZone: "subzone"}, - l2: Locality{Region: "region", Zone: "zone2", SubZone: "subzone"}, - wantEq: false, - }, - { - name: "different_subzones", - l1: Locality{Region: "region", Zone: "zone", SubZone: "subzone1"}, - l2: Locality{Region: "region", Zone: "zone", SubZone: "subzone2"}, - wantEq: false, - }, - { - name: "one_empty", - l1: Locality{}, - l2: Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, - wantEq: false, - }, - { - name: "both_empty", - l1: Locality{}, - l2: Locality{}, - wantEq: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if gotEq := test.l1.equal(test.l2); gotEq != test.wantEq { - t.Errorf("Equal() = %v, want %v", gotEq, test.wantEq) - } - }) - } -} - -func (s) TestNode_ToProto(t *testing.T) { - tests := []struct { - desc string - inputNode Node - wantProto *v3corepb.Node - }{ - { - desc: "all_fields_set", - inputNode: Node{ - ID: "id", - Cluster: "cluster", - Locality: Locality{ - Region: "region", - Zone: "zone", - SubZone: "sub_zone", - }, - Metadata: newStructProtoFromMap(t, map[string]any{"k1": "v1", "k2": 101, "k3": 280.0}), - UserAgentName: "user agent", - UserAgentVersion: "version", - clientFeatures: []string{"feature1", "feature2"}, - }, - wantProto: &v3corepb.Node{ - Id: "id", - Cluster: "cluster", - Locality: &v3corepb.Locality{ - Region: "region", - Zone: "zone", - SubZone: "sub_zone", - }, - Metadata: newStructProtoFromMap(t, map[string]any{"k1": "v1", "k2": 101, "k3": 280.0}), - UserAgentName: "user agent", - UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "version"}, - ClientFeatures: []string{"feature1", "feature2"}, - }, - }, - { - desc: "some_fields_unset", - inputNode: Node{ - ID: "id", - }, - wantProto: &v3corepb.Node{ - Id: "id", - UserAgentName: "", - UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: ""}, - ClientFeatures: nil, - }, - }, - { - desc: "empty_locality", - inputNode: Node{ - ID: "id", - Locality: Locality{}, - }, - wantProto: &v3corepb.Node{ - Id: "id", - UserAgentName: "", - UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: ""}, - ClientFeatures: nil, - }, - }, - } - - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - gotProto := test.inputNode.toProto() - if diff := cmp.Diff(test.wantProto, gotProto, protocmp.Transform()); diff != "" { - t.Fatalf("Unexpected diff in node proto: (-want, +got):\n%s", diff) - } - }) - } -} diff --git a/xds/internal/clients/grpctransport/grpc_transport.go b/xds/internal/clients/grpctransport/grpc_transport.go index c5c1f99694ba..9040be3cd673 100644 --- a/xds/internal/clients/grpctransport/grpc_transport.go +++ b/xds/internal/clients/grpctransport/grpc_transport.go @@ -31,38 +31,38 @@ import ( "google.golang.org/grpc/xds/internal/clients" ) -// ServerConfigExtension holds settings for connecting to a gRPC server, +// ServerIdentifierExtension holds settings for connecting to a gRPC server, // such as an xDS management or an LRS server. -type ServerConfigExtension struct { +type ServerIdentifierExtension struct { // Credentials will be used for all gRPC transports. If it is unset, // transport creation will fail. Credentials credentials.Bundle } -// Builder creates gRPC-based Transports. It must be paired with ServerConfigs -// that contain an Extension field of type ServerConfigExtension. +// Builder creates gRPC-based Transports. It must be paired with ServerIdentifiers +// that contain an Extension field of type ServerIdentifierExtension. type Builder struct{} // Build returns a gRPC-based clients.Transport. // -// The Extension field of the ServerConfig must be a ServerConfigExtension. -func (b *Builder) Build(sc clients.ServerConfig) (clients.Transport, error) { - if sc.ServerURI == "" { - return nil, fmt.Errorf("grpctransport: ServerURI is not set in ServerConfig") +// The Extension field of the ServerIdentifier must be a ServerIdentifierExtension. +func (b *Builder) Build(si clients.ServerIdentifier) (clients.Transport, error) { + if si.ServerURI == "" { + return nil, fmt.Errorf("grpctransport: ServerURI is not set in ServerIdentifier") } - if sc.Extensions == nil { - return nil, fmt.Errorf("grpctransport: Extensions is not set in ServerConfig") + if si.Extensions == nil { + return nil, fmt.Errorf("grpctransport: Extensions is not set in ServerIdentifier") } - sce, ok := sc.Extensions.(ServerConfigExtension) + sce, ok := si.Extensions.(ServerIdentifierExtension) if !ok { - return nil, fmt.Errorf("grpctransport: Extensions field is %T, but must be %T in ServerConfig", sc.Extensions, ServerConfigExtension{}) + return nil, fmt.Errorf("grpctransport: Extensions field is %T, but must be %T in ServerIdentifier", si.Extensions, ServerIdentifierExtension{}) } if sce.Credentials == nil { - return nil, fmt.Errorf("grptransport: Credentials field is not set in ServerConfigExtension") + return nil, fmt.Errorf("grptransport: Credentials field is not set in ServerIdentifierExtension") } // TODO: Incorporate reference count map for existing transports and - // deduplicate transports based on the provided ServerConfig so that + // deduplicate transports based on the provided ServerIdentifier so that // transport channel to same server can be shared between xDS and LRS // client. @@ -74,9 +74,9 @@ func (b *Builder) Build(sc clients.ServerConfig) (clients.Transport, error) { Time: 5 * time.Minute, Timeout: 20 * time.Second, }) - cc, err := grpc.NewClient(sc.ServerURI, kpCfg, grpc.WithCredentialsBundle(sce.Credentials), grpc.WithDefaultCallOptions(grpc.ForceCodec(&byteCodec{}))) + cc, err := grpc.NewClient(si.ServerURI, kpCfg, grpc.WithCredentialsBundle(sce.Credentials), grpc.WithDefaultCallOptions(grpc.ForceCodec(&byteCodec{}))) if err != nil { - return nil, fmt.Errorf("grpctransport: failed to create transport to server %q: %v", sc.ServerURI, err) + return nil, fmt.Errorf("grpctransport: failed to create transport to server %q: %v", si.ServerURI, err) } return &grpcTransport{cc: cc}, nil diff --git a/xds/internal/clients/grpctransport/grpc_transport_test.go b/xds/internal/clients/grpctransport/grpc_transport_test.go index 0ab48707c05e..cb308d8e60ef 100644 --- a/xds/internal/clients/grpctransport/grpc_transport_test.go +++ b/xds/internal/clients/grpctransport/grpc_transport_test.go @@ -105,6 +105,8 @@ func (s *testServer) StreamAggregatedResources(stream v3discoverygrpc.Aggregated return err // Handle other errors } + // Push received request for client to verify the correct request was + // received. select { case s.requestChan <- req: case <-ctx.Done(): @@ -130,9 +132,9 @@ func (tc *testCredentials) TransportCredentials() credentials.TransportCredentia // TestBuild_Success verifies that the Builder successfully creates a new // Transport with a non-nil grpc.ClientConn. func (s) TestBuild_Success(t *testing.T) { - serverCfg := clients.ServerConfig{ + serverCfg := clients.ServerIdentifier{ ServerURI: "server-address", - Extensions: ServerConfigExtension{Credentials: &testCredentials{transportCredentials: local.NewCredentials()}}, + Extensions: ServerIdentifierExtension{Credentials: &testCredentials{transportCredentials: local.NewCredentials()}}, } b := &Builder{} @@ -151,41 +153,41 @@ func (s) TestBuild_Success(t *testing.T) { } // TestBuild_Failure verifies that the Builder returns error when incorrect -// ServerConfig is provided. +// ServerIdentifier is provided. // // It covers the following scenarios: // - ServerURI is empty. // - Extensions is nil. -// - Extensions is not ServerConfigExtension. +// - Extensions is not ServerIdentifierExtension. // - Credentials are nil. func (s) TestBuild_Failure(t *testing.T) { tests := []struct { name string - serverCfg clients.ServerConfig + serverCfg clients.ServerIdentifier }{ { name: "ServerURI is empty", - serverCfg: clients.ServerConfig{ + serverCfg: clients.ServerIdentifier{ ServerURI: "", - Extensions: ServerConfigExtension{Credentials: insecure.NewBundle()}, + Extensions: ServerIdentifierExtension{Credentials: insecure.NewBundle()}, }, }, { name: "Extensions is nil", - serverCfg: clients.ServerConfig{ServerURI: "server-address"}, + serverCfg: clients.ServerIdentifier{ServerURI: "server-address"}, }, { - name: "Extensions is not a ServerConfigExtension", - serverCfg: clients.ServerConfig{ + name: "Extensions is not a ServerIdentifierExtension", + serverCfg: clients.ServerIdentifier{ ServerURI: "server-address", Extensions: 1, }, }, { - name: "ServerConfigExtension Credentials is nil", - serverCfg: clients.ServerConfig{ + name: "ServerIdentifierExtension Credentials is nil", + serverCfg: clients.ServerIdentifier{ ServerURI: "server-address", - Extensions: ServerConfigExtension{}, + Extensions: ServerIdentifierExtension{}, }, }, } @@ -208,9 +210,9 @@ func (s) TestBuild_Failure(t *testing.T) { func (s) TestNewStream_Success(t *testing.T) { ts := setupTestServer(t, &v3discoverypb.DiscoveryResponse{VersionInfo: "1"}) - serverCfg := clients.ServerConfig{ + serverCfg := clients.ServerIdentifier{ ServerURI: ts.address, - Extensions: ServerConfigExtension{Credentials: insecure.NewBundle()}, + Extensions: ServerIdentifierExtension{Credentials: insecure.NewBundle()}, } builder := Builder{} transport, err := builder.Build(serverCfg) @@ -229,9 +231,9 @@ func (s) TestNewStream_Success(t *testing.T) { // TestNewStream_Error verifies that NewStream() returns an error // when attempting to create a stream with an invalid server URI. func (s) TestNewStream_Error(t *testing.T) { - serverCfg := clients.ServerConfig{ + serverCfg := clients.ServerIdentifier{ ServerURI: "invalid-server-uri", - Extensions: ServerConfigExtension{Credentials: insecure.NewBundle()}, + Extensions: ServerIdentifierExtension{Credentials: insecure.NewBundle()}, } builder := Builder{} transport, err := builder.Build(serverCfg) @@ -262,9 +264,9 @@ func (s) TestStream_SendAndRecv(t *testing.T) { ts := setupTestServer(t, &v3discoverypb.DiscoveryResponse{VersionInfo: "1"}) // Build a grpc-based transport to the above server. - serverCfg := clients.ServerConfig{ + serverCfg := clients.ServerIdentifier{ ServerURI: ts.address, - Extensions: ServerConfigExtension{Credentials: insecure.NewBundle()}, + Extensions: ServerIdentifierExtension{Credentials: insecure.NewBundle()}, } builder := Builder{} transport, err := builder.Build(serverCfg) diff --git a/xds/internal/clients/internal/internal.go b/xds/internal/clients/internal/internal.go new file mode 100644 index 000000000000..d712129843ba --- /dev/null +++ b/xds/internal/clients/internal/internal.go @@ -0,0 +1,84 @@ +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package internal contains helpers for xDS and LRS clients. +package internal + +import ( + "fmt" + "strings" + + "google.golang.org/grpc/xds/internal/clients" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" +) + +// ServerIdentifierString returns a string representation of the +// clients.ServerIdentifier si. +// +// WARNING: This method is primarily intended for logging and testing +// purposes. The output returned by this method is not guaranteed to be stable +// and may change at any time. Do not rely on it for production use. +func ServerIdentifierString(si clients.ServerIdentifier) string { + return strings.Join([]string{si.ServerURI, fmt.Sprintf("%v", si.Extensions)}, "-") +} + +// NodeProto returns a protobuf representation of clients.Node n. +// +// This function is intended to be used by the client implementation to convert +// the user-provided Node configuration to its protobuf representation. +func NodeProto(n clients.Node) *v3corepb.Node { + return &v3corepb.Node{ + Id: n.ID, + Cluster: n.Cluster, + Locality: func() *v3corepb.Locality { + if isLocalityEmpty(n.Locality) { + return nil + } + return &v3corepb.Locality{ + Region: n.Locality.Region, + Zone: n.Locality.Zone, + SubZone: n.Locality.SubZone, + } + }(), + Metadata: func() *structpb.Struct { + if n.Metadata == nil { + return nil + } + if md, ok := n.Metadata.(*structpb.Struct); ok { + return proto.Clone(md).(*structpb.Struct) + } + return nil + }(), + UserAgentName: n.UserAgentName, + UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: n.UserAgentVersion}, + } +} + +// isLocalityEqual reports whether clients.Locality l is considered empty. +func isLocalityEmpty(l clients.Locality) bool { + return isLocalityEqual(l, clients.Locality{}) +} + +// isLocalityEqual returns true if clients.Locality l1 and l2 are considered +// equal. +func isLocalityEqual(l1, l2 clients.Locality) bool { + return l1.Region == l2.Region && l1.Zone == l2.Zone && l1.SubZone == l2.SubZone +} diff --git a/xds/internal/clients/internal/internal_test.go b/xds/internal/clients/internal/internal_test.go new file mode 100644 index 000000000000..8815b925114b --- /dev/null +++ b/xds/internal/clients/internal/internal_test.go @@ -0,0 +1,215 @@ +/* + * + * Copyright 2024 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package internal + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/xds/internal/clients" + + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + + v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +func newStructProtoFromMap(t *testing.T, input map[string]any) *structpb.Struct { + t.Helper() + + ret, err := structpb.NewStruct(input) + if err != nil { + t.Fatalf("Failed to create new struct proto from map %v: %v", input, err) + } + return ret +} + +func (s) TestIsLocalityEmpty(t *testing.T) { + tests := []struct { + name string + locality clients.Locality + want bool + }{ + { + name: "empty_locality", + locality: clients.Locality{}, + want: true, + }, + { + name: "non_empty_region", + locality: clients.Locality{Region: "region"}, + want: false, + }, + { + name: "non_empty_zone", + locality: clients.Locality{Zone: "zone"}, + want: false, + }, + { + name: "non_empty_subzone", + locality: clients.Locality{SubZone: "subzone"}, + want: false, + }, + { + name: "non_empty_all_fields", + locality: clients.Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, + want: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := isLocalityEmpty(test.locality); got != test.want { + t.Errorf("IsEmpty() = %v, want %v", got, test.want) + } + }) + } +} + +func (s) TestIsLocalityEqual(t *testing.T) { + tests := []struct { + name string + l1 clients.Locality + l2 clients.Locality + wantEq bool + }{ + { + name: "both_equal", + l1: clients.Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, + l2: clients.Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, + wantEq: true, + }, + { + name: "different_regions", + l1: clients.Locality{Region: "region1", Zone: "zone", SubZone: "subzone"}, + l2: clients.Locality{Region: "region2", Zone: "zone", SubZone: "subzone"}, + wantEq: false, + }, + + { + name: "different_zones", + l1: clients.Locality{Region: "region", Zone: "zone1", SubZone: "subzone"}, + l2: clients.Locality{Region: "region", Zone: "zone2", SubZone: "subzone"}, + wantEq: false, + }, + { + name: "different_subzones", + l1: clients.Locality{Region: "region", Zone: "zone", SubZone: "subzone1"}, + l2: clients.Locality{Region: "region", Zone: "zone", SubZone: "subzone2"}, + wantEq: false, + }, + { + name: "one_empty", + l1: clients.Locality{}, + l2: clients.Locality{Region: "region", Zone: "zone", SubZone: "subzone"}, + wantEq: false, + }, + { + name: "both_empty", + l1: clients.Locality{}, + l2: clients.Locality{}, + wantEq: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if gotEq := isLocalityEqual(test.l1, test.l2); gotEq != test.wantEq { + t.Errorf("Equal() = %v, want %v", gotEq, test.wantEq) + } + }) + } +} + +func (s) TestNodeProto(t *testing.T) { + tests := []struct { + desc string + inputNode clients.Node + wantProto *v3corepb.Node + }{ + { + desc: "all_fields_set", + inputNode: clients.Node{ + ID: "id", + Cluster: "cluster", + Locality: clients.Locality{ + Region: "region", + Zone: "zone", + SubZone: "sub_zone", + }, + Metadata: newStructProtoFromMap(t, map[string]any{"k1": "v1", "k2": 101, "k3": 280.0}), + UserAgentName: "user agent", + UserAgentVersion: "version", + }, + wantProto: &v3corepb.Node{ + Id: "id", + Cluster: "cluster", + Locality: &v3corepb.Locality{ + Region: "region", + Zone: "zone", + SubZone: "sub_zone", + }, + Metadata: newStructProtoFromMap(t, map[string]any{"k1": "v1", "k2": 101, "k3": 280.0}), + UserAgentName: "user agent", + UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "version"}, + }, + }, + { + desc: "some_fields_unset", + inputNode: clients.Node{ + ID: "id", + }, + wantProto: &v3corepb.Node{ + Id: "id", + UserAgentName: "", + UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: ""}, + }, + }, + { + desc: "empty_locality", + inputNode: clients.Node{ + ID: "id", + Locality: clients.Locality{}, + }, + wantProto: &v3corepb.Node{ + Id: "id", + UserAgentName: "", + UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: ""}, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + gotProto := NodeProto(test.inputNode) + if diff := cmp.Diff(test.wantProto, gotProto, protocmp.Transform()); diff != "" { + t.Fatalf("Unexpected diff in node proto: (-want, +got):\n%s", diff) + } + }) + } +} diff --git a/xds/internal/clients/lrsclient/load_store.go b/xds/internal/clients/lrsclient/load_store.go new file mode 100644 index 000000000000..d52db0c78330 --- /dev/null +++ b/xds/internal/clients/lrsclient/load_store.go @@ -0,0 +1,80 @@ +//revive:disable:unused-parameter + +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package lrsclient + +import "context" + +// A LoadStore aggregates loads for multiple clusters and services that are +// intended to be reported via LRS. +// +// LoadStore stores loads reported to a single LRS server. Use multiple stores +// for multiple servers. +// +// It is safe for concurrent use. +type LoadStore struct { +} + +// Stop stops the LRS stream associated with this LoadStore. +// +// If this LoadStore is the only one using the underlying LRS stream, the +// stream will be closed. If other LoadStores are also using the same stream, +// the reference count to the stream is decremented, and the stream remains +// open until all LoadStores have called Stop(). +// +// If this is the last LoadStore for the stream, this method makes a last +// attempt to flush any unreported load data to the LRS server. It will either +// wait for this attempt to complete, or for the provided context to be done +// before canceling the LRS stream. +func (ls *LoadStore) Stop(ctx context.Context) error { + panic("unimplemented") +} + +// ReporterForCluster returns the PerClusterReporter for the given cluster and +// service. +func (ls *LoadStore) ReporterForCluster(clusterName, serviceName string) PerClusterReporter { + panic("unimplemented") +} + +// PerClusterReporter records load data pertaining to a single cluster. It +// provides methods to record call starts, finishes, server-reported loads, +// and dropped calls. +type PerClusterReporter struct { +} + +// CallStarted records a call started in the LoadStore. +func (p *PerClusterReporter) CallStarted(locality string) { + panic("unimplemented") +} + +// CallFinished records a call finished in the LoadStore. +func (p *PerClusterReporter) CallFinished(locality string, err error) { + panic("unimplemented") +} + +// CallServerLoad records the server load in the LoadStore. +func (p *PerClusterReporter) CallServerLoad(locality, name string, val float64) { + panic("unimplemented") +} + +// CallDropped records a call dropped in the LoadStore. +func (p *PerClusterReporter) CallDropped(category string) { + panic("unimplemented") +} diff --git a/xds/internal/clients/lrsclient/lrsclient.go b/xds/internal/clients/lrsclient/lrsclient.go new file mode 100644 index 000000000000..5bd8aa60dcc2 --- /dev/null +++ b/xds/internal/clients/lrsclient/lrsclient.go @@ -0,0 +1,39 @@ +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package lrsclient provides an LRS (Load Reporting Service) client. +// +// See: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/load_stats/v3/lrs.proto +package lrsclient + +import "google.golang.org/grpc/xds/internal/clients" + +// LRSClient is an LRS (Load Reporting Service) client. +type LRSClient struct { +} + +// New returns a new LRS Client configured with the provided config. +func New(_ Config) (*LRSClient, error) { + panic("unimplemented") +} + +// ReportLoad creates a new load reporting stream for the provided server. It +// creates and returns a LoadStore for the caller to report loads. +func (*LRSClient) ReportLoad(_ clients.ServerIdentifier) *LoadStore { + panic("unimplemented") +} diff --git a/xds/internal/clients/lrsclient/lrsconfig.go b/xds/internal/clients/lrsclient/lrsconfig.go new file mode 100644 index 000000000000..c4862ff76794 --- /dev/null +++ b/xds/internal/clients/lrsclient/lrsconfig.go @@ -0,0 +1,35 @@ +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package lrsclient + +import ( + "google.golang.org/grpc/xds/internal/clients" +) + +// Config is used to configure an LRS client. After one has been passed to the +// LRS client's New function, no part of it may modified. A Config may be +// reused; the lrsclient package will also not modify it. +type Config struct { + // Node is the identity of the client application reporting load to the + // LRS server. + Node clients.Node + + // TransportBuilder is used to connect to the LRS server. + TransportBuilder clients.TransportBuilder +} diff --git a/xds/internal/clients/transport_builder.go b/xds/internal/clients/transport_builder.go index 5b0eb10cbd3c..10a25fcab1dc 100644 --- a/xds/internal/clients/transport_builder.go +++ b/xds/internal/clients/transport_builder.go @@ -23,19 +23,19 @@ import ( ) // TransportBuilder provides the functionality to create a communication -// channel to an xDS management server. +// channel to an xDS or LRS server. type TransportBuilder interface { - // Build creates a new [Transport] instance to the xDS server based on the - // provided ServerConfig. - Build(ServerConfig ServerConfig) (Transport, error) + // Build creates a new Transport instance to the server based on the + // provided ServerIdentifier. + Build(serverIdentifier ServerIdentifier) (Transport, error) } -// Transport provides the functionality to communicate with an xDS server using -// streaming calls. +// Transport provides the functionality to communicate with an xDS or LRS +// server using streaming calls. type Transport interface { - // NewStream creates a new streaming call to the xDS server for the - // specified RPC method name. The returned Stream interface can be used - // to send and receive messages on the stream. + // NewStream creates a new streaming call to the server for the specific + // RPC method name. The returned Stream interface can be used to send and + // receive messages on the stream. NewStream(context.Context, string) (Stream, error) // Close closes the Transport. @@ -43,7 +43,7 @@ type Transport interface { } // Stream provides methods to send and receive messages on a stream. Messages -// are represented as a byte slice ([]byte). +// are represented as a byte slice. type Stream interface { // Send sends the provided message on the stream. Send([]byte) error diff --git a/xds/internal/clients/xdsclient/resource_type.go b/xds/internal/clients/xdsclient/resource_type.go new file mode 100644 index 000000000000..8ca466ed716e --- /dev/null +++ b/xds/internal/clients/xdsclient/resource_type.go @@ -0,0 +1,92 @@ +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package xdsclient + +// ResourceType wraps all resource-type specific functionality. Each supported +// resource type needs to provide an implementation of the Decoder. +type ResourceType struct { + // TypeURL is the xDS type URL of this resource type for the v3 xDS + // protocol. This URL is used as the key to look up the corresponding + // ResourceType implementation in the ResourceTypes map provided in the + // Config. + TypeURL string + + // TODO: Revisit if we need TypeURL to be part of the struct because it is + // already a key in config's ResouceTypes map. + + // TypeName is a shorter representation of the TypeURL to identify the + // resource type. It is used for logging/debugging purposes. + TypeName string + + // AllResourcesRequiredInSotW indicates whether this resource type requires + // that all resources be present in every SotW response from the server. If + // true, a response that does not include a previously seen resource will + // be interpreted as a deletion of that resource. + AllResourcesRequiredInSotW bool + + // Decoder is used to deserialize and validate an xDS resource received + // from the xDS management server. + Decoder Decoder +} + +// Decoder wraps the resource-type specific functionality for validation +// and deserialization. +type Decoder interface { + // Decode deserializes and validates an xDS resource as received from the + // xDS management server. + // + // If deserialization fails or resource validation fails, it returns a + // non-nil error. Otherwise, returns a fully populated DecodeResult. + Decode(resource []byte, options DecodeOptions) (*DecodeResult, error) +} + +// DecodeOptions wraps the options required by ResourceType implementations for +// decoding configuration received from the xDS management server. +type DecodeOptions struct { + // Config contains the complete configuration passed to the xDS client. + // This contains useful data for resource validation. + Config *Config + + // ServerConfig contains the configuration of the xDS server that provided + // the current resource being decoded. + ServerConfig *ServerConfig +} + +// DecodeResult is the result of a decode operation. +type DecodeResult struct { + // Name is the name of the decoded resource. + Name string + + // Resource contains the configuration associated with the decoded + // resource. + Resource ResourceData +} + +// ResourceData contains the configuration data sent by the xDS management +// server, associated with the resource being watched. Every resource type must +// provide an implementation of this interface to represent the configuration +// received from the xDS management server. +type ResourceData interface { + // Equal returns true if the passed in resource data is equal to that of + // the receiver. + Equal(other ResourceData) bool + + // Bytes returns the underlying raw bytes of the resource proto. + Bytes() []byte +} diff --git a/xds/internal/clients/xdsclient/resource_watcher.go b/xds/internal/clients/xdsclient/resource_watcher.go new file mode 100644 index 000000000000..37d01bc71e76 --- /dev/null +++ b/xds/internal/clients/xdsclient/resource_watcher.go @@ -0,0 +1,45 @@ +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package xdsclient + +// ResourceWatcher is notified of the resource updates and errors that are +// received by the xDS client from the management server. +// +// All methods contain a done parameter which should be called when processing +// of the update has completed. For example, if processing a resource requires +// watching new resources, those watches should be completed before done is +// called, which can happen after the ResourceWatcher method has returned. +// Failure to call done will prevent the xDS client from providing future +// ResourceWatcher notifications. +type ResourceWatcher interface { + // ResourceChanged indicates a new version of the resource is available. + ResourceChanged(resourceData ResourceData, done func()) + + // ResourceError indicates an error occurred while trying to fetch or + // decode the associated resource. The previous version of the resource + // should be considered invalid. + ResourceError(err error, done func()) + + // AmbientError indicates an error occurred after a resource has been + // received that should not modify the use of that resource but may provide + // useful information about the state of the XDSClient for debugging + // purposes. The previous version of the resource should still be + // considered valid. + AmbientError(err error, done func()) +} diff --git a/xds/internal/clients/xdsclient/xdsclient.go b/xds/internal/clients/xdsclient/xdsclient.go new file mode 100644 index 000000000000..f893e8c925fc --- /dev/null +++ b/xds/internal/clients/xdsclient/xdsclient.go @@ -0,0 +1,67 @@ +//revive:disable:unused-parameter + +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package xdsclient provides an xDS (* Discovery Service) client. +// +// It allows applications to: +// - Create xDS client instances with in-memory configurations. +// - Register watches for named resources. +// - Receive resources via an ADS (Aggregated Discovery Service) stream. +// - Register watches for named resources (e.g. listeners, routes, or +// clusters). +// +// This enables applications to dynamically discover and configure resources +// such as listeners, routes, clusters, and endpoints from an xDS management +// server. +package xdsclient + +// XDSClient is a client which queries a set of discovery APIs (collectively +// termed as xDS) on a remote management server, to discover +// various dynamic resources. +type XDSClient struct { +} + +// New returns a new xDS Client configured with the provided config. +func New(config Config) (*XDSClient, error) { + panic("unimplemented") +} + +// WatchResource starts watching the specified resource. +// +// typeURL specifies the resource type implementation to use. The watch fails +// if there is no resource type implementation for the given typeURL. See the +// ResourceTypes field in the Config struct used to create the XDSClient. +// +// The returned function cancels the watch and prevents future calls to the +// watcher. +func (c *XDSClient) WatchResource(typeURL, name string, watcher ResourceWatcher) (cancel func()) { + panic("unimplemented") +} + +// Close closes the xDS client. +func (c *XDSClient) Close() error { + panic("unimplemented") +} + +// DumpResources returns the status and contents of all xDS resources being +// watched by the xDS client. +func (c *XDSClient) DumpResources() []byte { + panic("unimplemented") +} diff --git a/xds/internal/clients/xdsclient/xdsconfig.go b/xds/internal/clients/xdsclient/xdsconfig.go new file mode 100644 index 000000000000..bfbe41679ed4 --- /dev/null +++ b/xds/internal/clients/xdsclient/xdsconfig.go @@ -0,0 +1,85 @@ +/* + * + * Copyright 2025 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package xdsclient + +import ( + "google.golang.org/grpc/xds/internal/clients" +) + +// Config is used to configure an xDS client. After one has been passed to the +// xDS client's New function, no part of it may be modified. A Config may be +// reused; the xdsclient package will also not modify it. +type Config struct { + // Servers specifies a list of xDS management servers to connect to. The + // order of the servers in this list reflects the order of preference of + // the data returned by those servers. The xDS client uses the first + // available server from the list. + // + // See gRFC A71 for more details on fallback behavior when the primary + // xDS server is unavailable. + // + // gRFC A71: https://github.com/grpc/proposal/blob/master/A71-xds-fallback.md + Servers []ServerConfig + + // Authorities defines the configuration for each xDS authority. Federated resources + // will be fetched from the servers specified by the corresponding Authority. + Authorities map[string]Authority + + // Node is the identity of the xDS client connecting to the xDS + // management server. + Node clients.Node + + // TransportBuilder is used to create connections to xDS management servers. + TransportBuilder clients.TransportBuilder + + // ResourceTypes is a map from resource type URLs to resource type + // implementations. Each resource type URL uniquely identifies a specific + // kind of xDS resource, and the corresponding resource type implementation + // provides logic for parsing, validating, and processing resources of that + // type. + // + // For example: "type.googleapis.com/envoy.config.listener.v3.Listener" + ResourceTypes map[string]ResourceType +} + +// ServerConfig contains configuration for an xDS management server. +type ServerConfig struct { + ServerIdentifier clients.ServerIdentifier + + // IgnoreResourceDeletion is a server feature which if set to true, + // indicates that resource deletion errors from xDS management servers can + // be ignored and cached resource data can be used. + // + // This will be removed in the future once we implement gRFC A88 + // and two new fields FailOnDataErrors and + // ResourceTimerIsTransientError will be introduced. + IgnoreResourceDeletion bool + + // TODO: Link to gRFC A88 +} + +// Authority contains configuration for an xDS control plane authority. +// +// See: https://www.envoyproxy.io/docs/envoy/latest/xds/core/v3/resource_locator.proto#xds-core-v3-resourcelocator +type Authority struct { + // XDSServers contains the list of server configurations for this authority. + // + // See Config.Servers for more details. + XDSServers []ServerConfig +}