github.com/google/cloudprober@v0.11.3/rds/client/client.go (about)

     1  // Copyright 2018-2021 The Cloudprober 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  /*
    16  Package client implements a ResourceDiscovery service (RDS) client.
    17  */
    18  package client
    19  
    20  import (
    21  	"context"
    22  	"crypto/tls"
    23  	"errors"
    24  	"fmt"
    25  	"math/rand"
    26  	"net"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/google/cloudprober/common/iputils"
    32  	"github.com/google/cloudprober/common/oauth"
    33  	"github.com/google/cloudprober/common/tlsconfig"
    34  	"github.com/google/cloudprober/logger"
    35  	configpb "github.com/google/cloudprober/rds/client/proto"
    36  	pb "github.com/google/cloudprober/rds/proto"
    37  	spb "github.com/google/cloudprober/rds/proto"
    38  	"github.com/google/cloudprober/targets/endpoint"
    39  	dnsRes "github.com/google/cloudprober/targets/resolver"
    40  	"google.golang.org/grpc"
    41  	"google.golang.org/grpc/credentials"
    42  	grpcoauth "google.golang.org/grpc/credentials/oauth"
    43  	"google.golang.org/protobuf/proto"
    44  )
    45  
    46  // globalResolver is a singleton DNS resolver that is used as the default
    47  // resolver by targets. It is a singleton because dnsRes.Resolver provides a
    48  // cache layer that is best shared by all probes.
    49  var (
    50  	globalResolver *dnsRes.Resolver
    51  )
    52  
    53  type cacheRecord struct {
    54  	ip          string
    55  	port        int
    56  	labels      map[string]string
    57  	lastUpdated time.Time
    58  }
    59  
    60  // Default RDS port
    61  const defaultRDSPort = "9314"
    62  
    63  // Client represents an RDS based client instance.
    64  type Client struct {
    65  	mu            sync.RWMutex
    66  	c             *configpb.ClientConf
    67  	serverOpts    *configpb.ClientConf_ServerOptions
    68  	dialOpts      []grpc.DialOption
    69  	cache         map[string]*cacheRecord
    70  	names         []string
    71  	listResources func(context.Context, *pb.ListResourcesRequest) (*pb.ListResourcesResponse, error)
    72  	lastModified  int64
    73  	resolver      *dnsRes.Resolver
    74  	l             *logger.Logger
    75  }
    76  
    77  // ListResourcesFunc is a function that takes ListResourcesRequest and returns
    78  // ListResourcesResponse.
    79  type ListResourcesFunc func(context.Context, *pb.ListResourcesRequest) (*pb.ListResourcesResponse, error)
    80  
    81  // refreshState refreshes the client cache.
    82  func (client *Client) refreshState(timeout time.Duration) {
    83  	ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
    84  	defer cancelFunc()
    85  
    86  	req := client.c.GetRequest()
    87  	req.IfModifiedSince = proto.Int64(client.lastModified)
    88  
    89  	response, err := client.listResources(ctx, req)
    90  	if err != nil {
    91  		client.l.Errorf("rds.client: error getting resources from RDS server: %v", err)
    92  		return
    93  	}
    94  	client.updateState(response)
    95  }
    96  
    97  func (client *Client) updateState(response *pb.ListResourcesResponse) {
    98  	client.mu.Lock()
    99  	defer client.mu.Unlock()
   100  
   101  	// If server doesn't support caching, response's last_modified will be 0 and
   102  	// we'll skip the following block.
   103  	if response.GetLastModified() != 0 && response.GetLastModified() <= client.lastModified {
   104  		client.l.Infof("rds_client: Not refreshing state. Local last-modified: %d, response's last-modified: %d.", client.lastModified, response.GetLastModified())
   105  		return
   106  	}
   107  
   108  	client.names = make([]string, len(response.GetResources()))
   109  	oldcache := client.cache
   110  	client.cache = make(map[string]*cacheRecord, len(response.GetResources()))
   111  
   112  	i := 0
   113  	for _, res := range response.GetResources() {
   114  		if oldRes, ok := client.cache[res.GetName()]; ok {
   115  			client.l.Warningf("Got resource (%s) again, ignoring this instance: {%v}. Previous record: %+v.", res.GetName(), res, *oldRes)
   116  			continue
   117  		}
   118  		if oldcache[res.GetName()] != nil && res.GetIp() != oldcache[res.GetName()].ip {
   119  			client.l.Infof("Resource (%s) ip has changed: %s -> %s.", res.GetName(), oldcache[res.GetName()].ip, res.GetIp())
   120  		}
   121  		client.cache[res.GetName()] = &cacheRecord{res.GetIp(), int(res.GetPort()), res.Labels, time.Unix(res.GetLastUpdated(), 0)}
   122  		client.names[i] = res.GetName()
   123  		i++
   124  	}
   125  	client.names = client.names[:i]
   126  	client.lastModified = response.GetLastModified()
   127  }
   128  
   129  // ListEndpoints returns the list of resources.
   130  func (client *Client) ListEndpoints() []endpoint.Endpoint {
   131  	client.mu.RLock()
   132  	defer client.mu.RUnlock()
   133  	result := make([]endpoint.Endpoint, len(client.names))
   134  	for i, name := range client.names {
   135  		result[i] = endpoint.Endpoint{Name: name, Port: client.cache[name].port, Labels: client.cache[name].labels, LastUpdated: client.cache[name].lastUpdated}
   136  	}
   137  	return result
   138  }
   139  
   140  // Resolve returns the IP address for the given resource. If no IP address is
   141  // associated with the resource, an error is returned.
   142  func (client *Client) Resolve(name string, ipVer int) (net.IP, error) {
   143  	client.mu.RLock()
   144  	defer client.mu.RUnlock()
   145  
   146  	cr, ok := client.cache[name]
   147  	if !ok || cr.ip == "" {
   148  		return nil, fmt.Errorf("no IP address for the resource: %s", name)
   149  	}
   150  
   151  	ip := net.ParseIP(cr.ip)
   152  	// If not a valid IP, use DNS resolver to resolve it.
   153  	if ip == nil {
   154  		return client.resolver.Resolve(cr.ip, ipVer)
   155  	}
   156  
   157  	if ipVer == 0 || iputils.IPVersion(ip) == ipVer {
   158  		return ip, nil
   159  	}
   160  
   161  	return nil, fmt.Errorf("no IPv%d address (IP: %s) for %s", ipVer, ip.String(), name)
   162  }
   163  
   164  func (client *Client) connect(serverAddr string) (*grpc.ClientConn, error) {
   165  	client.l.Infof("rds.client: using RDS servers at: %s", serverAddr)
   166  
   167  	if strings.HasPrefix(serverAddr, "srvlist:///") {
   168  		client.dialOpts = append(client.dialOpts, grpc.WithResolvers(&srvListBuilder{defaultPort: defaultRDSPort}))
   169  	}
   170  
   171  	return grpc.Dial(client.serverOpts.GetServerAddress(), client.dialOpts...)
   172  }
   173  
   174  // initListResourcesFunc uses server options to establish a connection with the
   175  // given RDS server.
   176  func (client *Client) initListResourcesFunc() error {
   177  	if client.listResources != nil {
   178  		return nil
   179  	}
   180  
   181  	if client.serverOpts == nil || client.serverOpts.GetServerAddress() == "" {
   182  		return errors.New("rds.Client: RDS server address not defined")
   183  	}
   184  
   185  	// Transport security options.
   186  	if client.serverOpts.GetTlsConfig() != nil {
   187  		tlsConfig := &tls.Config{}
   188  		if err := tlsconfig.UpdateTLSConfig(tlsConfig, client.serverOpts.GetTlsConfig(), false); err != nil {
   189  			return fmt.Errorf("rds/client: error initializing TLS config (%+v): %v", client.serverOpts.GetTlsConfig(), err)
   190  		}
   191  		client.dialOpts = append(client.dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
   192  	} else {
   193  		client.dialOpts = append(client.dialOpts, grpc.WithInsecure())
   194  	}
   195  
   196  	// OAuth related options.
   197  	if client.serverOpts.GetOauthConfig() != nil {
   198  		oauthTS, err := oauth.TokenSourceFromConfig(client.serverOpts.GetOauthConfig(), client.l)
   199  		if err != nil {
   200  			return fmt.Errorf("rds/client: error getting token source from OAuth config (%+v): %v", client.serverOpts.GetOauthConfig(), err)
   201  		}
   202  		client.dialOpts = append(client.dialOpts, grpc.WithPerRPCCredentials(grpcoauth.TokenSource{oauthTS}))
   203  	}
   204  
   205  	conn, err := client.connect(client.serverOpts.GetServerAddress())
   206  	if err != nil {
   207  		return fmt.Errorf("rds/client: error connecting to server (%v): %v", client.serverOpts.GetServerAddress(), err)
   208  	}
   209  
   210  	client.listResources = func(ctx context.Context, in *pb.ListResourcesRequest) (*pb.ListResourcesResponse, error) {
   211  		return spb.NewResourceDiscoveryClient(conn).ListResources(ctx, in)
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  // New creates an RDS (ResourceDiscovery service) client instance and set it up
   218  // for continuous refresh.
   219  func New(c *configpb.ClientConf, listResources ListResourcesFunc, l *logger.Logger) (*Client, error) {
   220  	client := &Client{
   221  		c:             c,
   222  		serverOpts:    c.GetServerOptions(),
   223  		cache:         make(map[string]*cacheRecord),
   224  		listResources: listResources,
   225  		resolver:      globalResolver,
   226  		l:             l,
   227  	}
   228  
   229  	if err := client.initListResourcesFunc(); err != nil {
   230  		return nil, fmt.Errorf("rds/client: error initializing listListResource function: %v", err)
   231  	}
   232  
   233  	reEvalInterval := time.Duration(client.c.GetReEvalSec()) * time.Second
   234  	client.refreshState(reEvalInterval)
   235  	go func() {
   236  		// Introduce a random delay between 0-reEvalInterval before starting the
   237  		// refreshState loop. If there are multiple cloudprober instances, this will
   238  		// make sure that each instance calls RDS server at a different point of
   239  		// time.
   240  		rand.Seed(time.Now().UnixNano())
   241  		randomDelaySec := rand.Intn(int(reEvalInterval.Seconds()))
   242  		time.Sleep(time.Duration(randomDelaySec) * time.Second)
   243  		for range time.Tick(reEvalInterval) {
   244  			client.refreshState(reEvalInterval)
   245  		}
   246  	}()
   247  
   248  	return client, nil
   249  }
   250  
   251  // init initializes the package by creating a new global resolver.
   252  func init() {
   253  	globalResolver = dnsRes.New()
   254  }