| 
 | 1 | +// Copyright 2022 Envoyproxy Authors  | 
 | 2 | +//  | 
 | 3 | +//   Licensed under the Apache License, Version 2.0 (the "License");  | 
 | 4 | +//   you may not use this file except in compliance with the License.  | 
 | 5 | +//   You may obtain a copy of the License at  | 
 | 6 | +//  | 
 | 7 | +//       http://www.apache.org/licenses/LICENSE-2.0  | 
 | 8 | +//  | 
 | 9 | +//   Unless required by applicable law or agreed to in writing, software  | 
 | 10 | +//   distributed under the License is distributed on an "AS IS" BASIS,  | 
 | 11 | +//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  | 
 | 12 | +//   See the License for the specific language governing permissions and  | 
 | 13 | +//   limitations under the License.  | 
 | 14 | + | 
 | 15 | +// Package sotw provides an implementation of GRPC SoTW (State of The World) part of XDS client  | 
 | 16 | +package sotw  | 
 | 17 | + | 
 | 18 | +import (  | 
 | 19 | +	"context"  | 
 | 20 | +	"errors"  | 
 | 21 | +	"io"  | 
 | 22 | +	"sync"  | 
 | 23 | + | 
 | 24 | +	"github.com/golang/protobuf/ptypes/any"  | 
 | 25 | +	status "google.golang.org/genproto/googleapis/rpc/status"  | 
 | 26 | +	"google.golang.org/grpc"  | 
 | 27 | +	"google.golang.org/grpc/codes"  | 
 | 28 | +	grpcStatus "google.golang.org/grpc/status"  | 
 | 29 | + | 
 | 30 | +	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"  | 
 | 31 | +	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"  | 
 | 32 | +)  | 
 | 33 | + | 
 | 34 | +var (  | 
 | 35 | +	ErrInit    = errors.New("ads client: grpc connection is not initialized (use InitConnect() method to initialize connection)")  | 
 | 36 | +	ErrNilResp = errors.New("ads client: nil response from xds management server")  | 
 | 37 | +)  | 
 | 38 | + | 
 | 39 | +// ADSClient is a SoTW and ADS based generic gRPC xDS client which can be used to  | 
 | 40 | +// implement an xDS client which fetches resources from an xDS server and responds  | 
 | 41 | +// the server back with ack or nack the resources.  | 
 | 42 | +type ADSClient interface {  | 
 | 43 | +	// Initialize the gRPC connection with management server and send the initial Discovery Request.  | 
 | 44 | +	InitConnect(clientConn grpc.ClientConnInterface, opts ...grpc.CallOption) error  | 
 | 45 | +	// Fetch waits for a response from management server and returns response or error.  | 
 | 46 | +	Fetch() (*Response, error)  | 
 | 47 | +	// Ack acknowledge the validity of the last received response to management server.  | 
 | 48 | +	Ack() error  | 
 | 49 | +	// Nack acknowledge the invalidity of the last received response to management server.  | 
 | 50 | +	Nack(message string) error  | 
 | 51 | +}  | 
 | 52 | + | 
 | 53 | +// Response wraps the latest Resources from the xDS server.  | 
 | 54 | +// For the time being it only contains the Resources from server. This can be extended with  | 
 | 55 | +// other response details. For example some metadata from DiscoveryResponse.  | 
 | 56 | +type Response struct {  | 
 | 57 | +	Resources []*any.Any  | 
 | 58 | +}  | 
 | 59 | + | 
 | 60 | +type adsClient struct {  | 
 | 61 | +	ctx     context.Context  | 
 | 62 | +	mu      sync.Mutex  | 
 | 63 | +	node    *core.Node  | 
 | 64 | +	typeURL string  | 
 | 65 | + | 
 | 66 | +	// streamClient is the ADS discovery client  | 
 | 67 | +	streamClient discovery.AggregatedDiscoveryService_StreamAggregatedResourcesClient  | 
 | 68 | +	// lastAckedResponse is the last response acked by the ADS client  | 
 | 69 | +	lastAckedResponse *discovery.DiscoveryResponse  | 
 | 70 | +	// lastReceivedResponse is the last response received from management server  | 
 | 71 | +	lastReceivedResponse *discovery.DiscoveryResponse  | 
 | 72 | +}  | 
 | 73 | + | 
 | 74 | +// NewADSClient returns a new ADSClient  | 
 | 75 | +func NewADSClient(ctx context.Context, node *core.Node, typeURL string) ADSClient {  | 
 | 76 | +	return &adsClient{  | 
 | 77 | +		ctx:     ctx,  | 
 | 78 | +		node:    node,  | 
 | 79 | +		typeURL: typeURL,  | 
 | 80 | +	}  | 
 | 81 | +}  | 
 | 82 | + | 
 | 83 | +// Initialize the gRPC connection with management server and send the initial Discovery Request.  | 
 | 84 | +func (c *adsClient) InitConnect(clientConn grpc.ClientConnInterface, opts ...grpc.CallOption) error {  | 
 | 85 | +	streamClient, err := discovery.NewAggregatedDiscoveryServiceClient(clientConn).StreamAggregatedResources(c.ctx, opts...)  | 
 | 86 | +	if err != nil {  | 
 | 87 | +		return err  | 
 | 88 | +	}  | 
 | 89 | +	c.streamClient = streamClient  | 
 | 90 | +	return c.Ack()  | 
 | 91 | +}  | 
 | 92 | + | 
 | 93 | +// Fetch waits for a response from management server and returns response or error.  | 
 | 94 | +func (c *adsClient) Fetch() (*Response, error) {  | 
 | 95 | +	c.mu.Lock()  | 
 | 96 | +	defer c.mu.Unlock()  | 
 | 97 | + | 
 | 98 | +	if c.streamClient == nil {  | 
 | 99 | +		return nil, ErrInit  | 
 | 100 | +	}  | 
 | 101 | +	resp, err := c.streamClient.Recv()  | 
 | 102 | +	if err != nil {  | 
 | 103 | +		return nil, err  | 
 | 104 | +	}  | 
 | 105 | + | 
 | 106 | +	if resp == nil {  | 
 | 107 | +		return nil, ErrNilResp  | 
 | 108 | +	}  | 
 | 109 | +	c.lastReceivedResponse = resp  | 
 | 110 | +	return &Response{  | 
 | 111 | +		Resources: resp.GetResources(),  | 
 | 112 | +	}, err  | 
 | 113 | +}  | 
 | 114 | + | 
 | 115 | +// Ack acknowledge the validity of the last received response to management server.  | 
 | 116 | +func (c *adsClient) Ack() error {  | 
 | 117 | +	c.mu.Lock()  | 
 | 118 | +	defer c.mu.Unlock()  | 
 | 119 | + | 
 | 120 | +	c.lastAckedResponse = c.lastReceivedResponse  | 
 | 121 | +	return c.send(nil)  | 
 | 122 | +}  | 
 | 123 | + | 
 | 124 | +// Nack acknowledge the invalidity of the last received response to management server.  | 
 | 125 | +func (c *adsClient) Nack(message string) error {  | 
 | 126 | +	c.mu.Lock()  | 
 | 127 | +	defer c.mu.Unlock()  | 
 | 128 | + | 
 | 129 | +	errorDetail := &status.Status{  | 
 | 130 | +		Message: message,  | 
 | 131 | +	}  | 
 | 132 | +	return c.send(errorDetail)  | 
 | 133 | +}  | 
 | 134 | + | 
 | 135 | +// IsConnError checks the provided error is due to the gRPC connection  | 
 | 136 | +// and returns true if it is due to the gRPC connection.  | 
 | 137 | +//  | 
 | 138 | +// In this case the gRPC connection with the server should be re initialized with the  | 
 | 139 | +// ADSClient.InitConnect method.  | 
 | 140 | +func IsConnError(err error) bool {  | 
 | 141 | +	if err == nil {  | 
 | 142 | +		return false  | 
 | 143 | +	}  | 
 | 144 | +	if errors.Is(err, io.EOF) {  | 
 | 145 | +		return true  | 
 | 146 | +	}  | 
 | 147 | +	errStatus, ok := grpcStatus.FromError(err)  | 
 | 148 | +	if !ok {  | 
 | 149 | +		return false  | 
 | 150 | +	}  | 
 | 151 | +	return errStatus.Code() == codes.Unavailable || errStatus.Code() == codes.Canceled  | 
 | 152 | +}  | 
 | 153 | + | 
 | 154 | +func (c *adsClient) send(errorDetail *status.Status) error {  | 
 | 155 | +	if c.streamClient == nil {  | 
 | 156 | +		return ErrInit  | 
 | 157 | +	}  | 
 | 158 | + | 
 | 159 | +	req := &discovery.DiscoveryRequest{  | 
 | 160 | +		Node:          c.node,  | 
 | 161 | +		VersionInfo:   c.lastAckedResponse.GetVersionInfo(),  | 
 | 162 | +		TypeUrl:       c.typeURL,  | 
 | 163 | +		ResponseNonce: c.lastReceivedResponse.GetNonce(),  | 
 | 164 | +		ErrorDetail:   errorDetail,  | 
 | 165 | +	}  | 
 | 166 | +	return c.streamClient.Send(req)  | 
 | 167 | +}  | 
0 commit comments