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 }