github.com/google/cloudprober@v0.11.3/targets/lameduck/lameduck.go (about)

     1  // Copyright 2017-2019 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  // Package lameduck implements a lameducks provider. Lameduck provider fetches
    16  // lameducks from the RTC (Runtime Configurator) service. This functionality
    17  // allows an operator to do hitless VM upgrades. If a target is set to be in
    18  // lameduck by the operator, it is taken out of the targets list.
    19  package lameduck
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"net/http"
    26  	"sync"
    27  
    28  	"cloud.google.com/go/compute/metadata"
    29  	"github.com/golang/protobuf/proto"
    30  	"github.com/google/cloudprober/config/runconfig"
    31  	"github.com/google/cloudprober/logger"
    32  	rdsclient "github.com/google/cloudprober/rds/client"
    33  	rdsclientpb "github.com/google/cloudprober/rds/client/proto"
    34  	"github.com/google/cloudprober/rds/gcp"
    35  	rdspb "github.com/google/cloudprober/rds/proto"
    36  	"github.com/google/cloudprober/rds/server"
    37  	serverconfigpb "github.com/google/cloudprober/rds/server/proto"
    38  	"github.com/google/cloudprober/targets/endpoint"
    39  	configpb "github.com/google/cloudprober/targets/lameduck/proto"
    40  	targetspb "github.com/google/cloudprober/targets/proto"
    41  	"github.com/google/cloudprober/targets/rtc/rtcservice"
    42  )
    43  
    44  // Lameducker provides an interface to Lameduck/Unlameduck an instance.
    45  //
    46  // Cloudprober doesn't currently (as of July, 2018) use this interface by
    47  // itself. It's provided here so that other software (e.g. probing deployment
    48  // management software) can lameduck/unlameduck instances in a way that
    49  // Cloudprober understands.
    50  type Lameducker interface {
    51  	Lameduck(name string) error
    52  	Unlameduck(name string) error
    53  }
    54  
    55  // global.lister is a singleton Lister. It caches data from the upstream config
    56  // service, allowing for multiple consumers to lookup for lameducks without
    57  // increasing load on the upstream service.
    58  var global struct {
    59  	mu     sync.RWMutex
    60  	lister endpoint.Lister
    61  }
    62  
    63  // service provides methods to do lameduck operations on VMs.
    64  type service struct {
    65  	rtc  rtcservice.Config
    66  	opts *configpb.Options
    67  	l    *logger.Logger
    68  }
    69  
    70  // Lameduck puts the target in lameduck mode.
    71  func (ldSvc *service) Lameduck(name string) error {
    72  	return ldSvc.rtc.Write(name, []byte{0})
    73  }
    74  
    75  // Unlameduck removes the target from lameduck mode.
    76  func (ldSvc *service) Unlameduck(name string) error {
    77  	err := ldSvc.rtc.Delete(name)
    78  	return err
    79  }
    80  
    81  // NewService creates a new lameduck service using the provided config options
    82  // and an oauth2 enabled *http.Client; if the client is set to nil, an oauth
    83  // enabled client is created automatically using GCP default credentials.
    84  func newService(opts *configpb.Options, proj string, hc *http.Client, l *logger.Logger) (*service, error) {
    85  	if opts == nil {
    86  		return nil, fmt.Errorf("lameduck.Init: failed to construct lameduck Service: no lameDuckOptions given")
    87  	}
    88  	if l == nil {
    89  		l = &logger.Logger{}
    90  	}
    91  
    92  	cfg := opts.GetRuntimeconfigName()
    93  
    94  	rtc, err := rtcservice.New(proj, cfg, hc)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("lameduck.Init : rtcconfig service initialization failed : %v", err)
    97  	}
    98  
    99  	return &service{
   100  		rtc:  rtc,
   101  		opts: opts,
   102  		l:    l,
   103  	}, nil
   104  }
   105  
   106  func getProject(opts *configpb.Options) (string, error) {
   107  	project := opts.GetRuntimeconfigProject()
   108  	if project == "" {
   109  		var err error
   110  		project, err = metadata.ProjectID()
   111  		if err != nil {
   112  			return "", fmt.Errorf("lameduck.getProject: error while getting project id: %v", err)
   113  		}
   114  	}
   115  	return project, nil
   116  }
   117  
   118  // NewLameducker creates a new lameducker using the provided config and an
   119  // oauth2 enabled *http.Client; if the client is set to nil, an oauth enabled
   120  // client is created automatically using GCP default credentials.
   121  func NewLameducker(opts *configpb.Options, hc *http.Client, l *logger.Logger) (Lameducker, error) {
   122  	project, err := getProject(opts)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	return newService(opts, project, hc, l)
   127  }
   128  
   129  func (li *lister) newRDSServer() (*server.Server, error) {
   130  	resTypes := make(map[string]string)
   131  	if li.rtcConfig != "" {
   132  		resTypes[gcp.ResourceTypes.RTCVariables] = li.rtcConfig
   133  	}
   134  	if li.pubsubTopic != "" {
   135  		resTypes[gcp.ResourceTypes.PubsubMessages] = li.pubsubTopic
   136  	}
   137  
   138  	pc := gcp.DefaultProviderConfig([]string{li.project}, resTypes, int(li.opts.GetReEvalSec()), "")
   139  	return server.New(context.Background(), &serverconfigpb.ServerConf{Provider: []*serverconfigpb.Provider{pc}}, nil, li.l)
   140  }
   141  
   142  func (li *lister) rdsClient(baseResourcePath string, additionalFilter *rdspb.Filter) (*rdsclient.Client, error) {
   143  	rdsClientConf := &rdsclientpb.ClientConf{
   144  		ServerOptions: li.rdsServerOpts,
   145  		Request: &rdspb.ListResourcesRequest{
   146  			Provider:     proto.String("gcp"),
   147  			ResourcePath: proto.String(fmt.Sprintf("%s/%s", baseResourcePath, li.project)),
   148  			Filter: []*rdspb.Filter{
   149  				{
   150  					Key:   proto.String("updated_within"),
   151  					Value: proto.String(fmt.Sprintf("%ds", li.opts.GetExpirationSec())),
   152  				},
   153  			},
   154  		},
   155  		ReEvalSec: proto.Int32(li.opts.GetReEvalSec()),
   156  	}
   157  
   158  	if additionalFilter != nil {
   159  		rdsClientConf.Request.Filter = append(rdsClientConf.Request.Filter, additionalFilter)
   160  	}
   161  
   162  	return rdsclient.New(rdsClientConf, li.listResourcesFunc, li.l)
   163  }
   164  
   165  func (li *lister) initClients() error {
   166  	if li.rtcConfig != "" {
   167  		li.l.Infof("lameduck: creating RDS client for RTC variables")
   168  
   169  		additionalFilter := &rdspb.Filter{
   170  			Key:   proto.String("config_name"),
   171  			Value: proto.String(li.opts.GetRuntimeconfigName()),
   172  		}
   173  
   174  		cl, err := li.rdsClient("rtc_variables", additionalFilter)
   175  		if err != nil {
   176  			return err
   177  		}
   178  		li.clients = append(li.clients, cl)
   179  	}
   180  
   181  	if li.pubsubTopic != "" {
   182  		li.l.Infof("lameduck: creating RDS client for PubSub messages")
   183  
   184  		// Here we assume that subscription name contains the topic name. This is
   185  		// true for the RDS implmentation.
   186  		additionalFilter := &rdspb.Filter{
   187  			Key:   proto.String("subscription"),
   188  			Value: proto.String(li.pubsubTopic),
   189  		}
   190  
   191  		cl, err := li.rdsClient("pubsub_messages", additionalFilter)
   192  		if err != nil {
   193  			return err
   194  		}
   195  		li.clients = append(li.clients, cl)
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func (li *lister) ListEndpoints() []endpoint.Endpoint {
   202  	var result []endpoint.Endpoint
   203  	for _, cl := range li.clients {
   204  		result = append(result, cl.ListEndpoints()...)
   205  	}
   206  
   207  	if len(result) != 0 {
   208  		li.l.Infof("Lameducked targets: %v", result)
   209  	}
   210  	return result
   211  }
   212  
   213  type lister struct {
   214  	opts              *configpb.Options
   215  	project           string
   216  	rtcConfig         string
   217  	pubsubTopic       string
   218  	rdsServerOpts     *rdsclientpb.ClientConf_ServerOptions
   219  	listResourcesFunc rdsclient.ListResourcesFunc
   220  	clients           []*rdsclient.Client
   221  	l                 *logger.Logger
   222  }
   223  
   224  func newLister(globalOpts *targetspb.GlobalTargetsOptions, l *logger.Logger) (*lister, error) {
   225  	opts := globalOpts.GetLameDuckOptions()
   226  	li := &lister{
   227  		opts:          opts,
   228  		rdsServerOpts: globalOpts.GetRdsServerOptions(),
   229  		rtcConfig:     opts.GetRuntimeconfigName(),
   230  		pubsubTopic:   opts.GetPubsubTopic(),
   231  		l:             l,
   232  	}
   233  
   234  	var err error
   235  	li.project, err = getProject(opts)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  
   240  	// If there are lameduck specific RDS server options, use them.
   241  	if li.opts.GetRdsServerOptions() != nil {
   242  		li.rdsServerOpts = li.opts.GetRdsServerOptions()
   243  	}
   244  
   245  	// If no RDS server options are configured, look for a local one.
   246  	if li.rdsServerOpts == nil {
   247  		localRDSServer := runconfig.LocalRDSServer()
   248  		if localRDSServer == nil {
   249  			li.l.Infof("rds_server_address not given and found no local RDS server, creating a new one.")
   250  
   251  			var err error
   252  			localRDSServer, err = li.newRDSServer()
   253  			if err != nil {
   254  				return nil, fmt.Errorf("error while creating local RDS server: %v", err)
   255  			}
   256  		}
   257  		li.listResourcesFunc = localRDSServer.ListResources
   258  	}
   259  
   260  	return li, li.initClients()
   261  }
   262  
   263  // InitDefaultLister initializes the package using the given arguments. If a
   264  // lister is given in the arguments, global.lister is set to that, otherwise a
   265  // new lameduck service is created using the config options, and global.lister
   266  // is set to that service. Initiating the package from a given lister is useful
   267  // for testing pacakges that depend on this package.
   268  func InitDefaultLister(globalOpts *targetspb.GlobalTargetsOptions, lister endpoint.Lister, l *logger.Logger) error {
   269  	global.mu.Lock()
   270  	defer global.mu.Unlock()
   271  
   272  	// Make sure we initialize global.lister only once.
   273  	if global.lister != nil {
   274  		return nil
   275  	}
   276  
   277  	// If a lister has been provided, use that. It's useful for testing.
   278  	if lister != nil {
   279  		global.lister = lister
   280  		return nil
   281  	}
   282  
   283  	if globalOpts.GetLameDuckOptions().GetUseRds() {
   284  		l.Warning("lameduck: use_rds doesn't do anything anymore and will soon be removed.")
   285  	}
   286  
   287  	lister, err := newLister(globalOpts, l)
   288  	if err != nil {
   289  		return err
   290  	}
   291  
   292  	global.lister = lister
   293  	return nil
   294  }
   295  
   296  // GetDefaultLister returns the global Lister. If global lister is
   297  // uninitialized, it returns an error.
   298  func GetDefaultLister() (endpoint.Lister, error) {
   299  	global.mu.RLock()
   300  	defer global.mu.RUnlock()
   301  	if global.lister == nil {
   302  		return nil, errors.New("global lameduck service not initialized")
   303  	}
   304  	return global.lister, nil
   305  }