github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/externalcontrollerupdater/externalcontrollerupdater.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package externalcontrollerupdater
     5  
     6  import (
     7  	"io"
     8  	"reflect"
     9  	"time"
    10  
    11  	"github.com/juju/clock"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"github.com/juju/names/v5"
    15  	"github.com/juju/worker/v3"
    16  	"github.com/juju/worker/v3/catacomb"
    17  
    18  	"github.com/juju/juju/api"
    19  	"github.com/juju/juju/api/controller/crosscontroller"
    20  	"github.com/juju/juju/core/crossmodel"
    21  	"github.com/juju/juju/core/watcher"
    22  	"github.com/juju/juju/rpc"
    23  )
    24  
    25  var logger = loggo.GetLogger("juju.worker.externalcontrollerupdater")
    26  
    27  // ExternalControllerUpdaterClient defines the interface for watching changes
    28  // to the local controller's external controller records, and obtaining and
    29  // updating their values. This will communicate only with the local controller.
    30  type ExternalControllerUpdaterClient interface {
    31  	WatchExternalControllers() (watcher.StringsWatcher, error)
    32  	ExternalControllerInfo(controllerUUID string) (*crossmodel.ControllerInfo, error)
    33  	SetExternalControllerInfo(crossmodel.ControllerInfo) error
    34  }
    35  
    36  // ExternalControllerWatcherClientCloser extends the ExternalControllerWatcherClient
    37  // interface with a Close method, for closing the API connection associated with
    38  // the client.
    39  type ExternalControllerWatcherClientCloser interface {
    40  	ExternalControllerWatcherClient
    41  	io.Closer
    42  }
    43  
    44  // ExternalControllerWatcherClient defines the interface for watching changes
    45  // to and obtaining the current API information for a controller. This will
    46  // communicate with an external controller.
    47  type ExternalControllerWatcherClient interface {
    48  	WatchControllerInfo() (watcher.NotifyWatcher, error)
    49  	ControllerInfo() (*crosscontroller.ControllerInfo, error)
    50  }
    51  
    52  // NewExternalControllerWatcherClientFunc is a function type that
    53  // returns an ExternalControllerWatcherClientCloser, given an
    54  // *api.Info. The api.Info should be for making a controller-only
    55  // connection to a remote/external controller.
    56  type NewExternalControllerWatcherClientFunc func(*api.Info) (ExternalControllerWatcherClientCloser, error)
    57  
    58  // New returns a new external controller updater worker.
    59  func New(
    60  	externalControllers ExternalControllerUpdaterClient,
    61  	newExternalControllerWatcherClient NewExternalControllerWatcherClientFunc,
    62  	clock clock.Clock,
    63  ) (worker.Worker, error) {
    64  	w := updaterWorker{
    65  		watchExternalControllers:           externalControllers.WatchExternalControllers,
    66  		externalControllerInfo:             externalControllers.ExternalControllerInfo,
    67  		setExternalControllerInfo:          externalControllers.SetExternalControllerInfo,
    68  		newExternalControllerWatcherClient: newExternalControllerWatcherClient,
    69  		runner: worker.NewRunner(worker.RunnerParams{
    70  			// One of the controller watchers fails should not
    71  			// prevent the others from running.
    72  			IsFatal: func(error) bool { return false },
    73  
    74  			// If the API connection fails, try again in 1 minute.
    75  			RestartDelay: time.Minute,
    76  			Clock:        clock,
    77  			Logger:       logger,
    78  		}),
    79  	}
    80  	if err := catacomb.Invoke(catacomb.Plan{
    81  		Site: &w.catacomb,
    82  		Work: w.loop,
    83  		Init: []worker.Worker{w.runner},
    84  	}); err != nil {
    85  		return nil, errors.Trace(err)
    86  	}
    87  	return &w, nil
    88  }
    89  
    90  type updaterWorker struct {
    91  	catacomb catacomb.Catacomb
    92  	runner   *worker.Runner
    93  
    94  	watchExternalControllers           func() (watcher.StringsWatcher, error)
    95  	externalControllerInfo             func(controllerUUID string) (*crossmodel.ControllerInfo, error)
    96  	setExternalControllerInfo          func(crossmodel.ControllerInfo) error
    97  	newExternalControllerWatcherClient NewExternalControllerWatcherClientFunc
    98  }
    99  
   100  // Kill is part of the worker.Worker interface.
   101  func (w *updaterWorker) Kill() {
   102  	w.catacomb.Kill(nil)
   103  }
   104  
   105  // Wait is part of the worker.Worker interface.
   106  func (w *updaterWorker) Wait() error {
   107  	return w.catacomb.Wait()
   108  }
   109  
   110  func (w *updaterWorker) loop() error {
   111  	watcher, err := w.watchExternalControllers()
   112  	if err != nil {
   113  		return errors.Annotate(err, "watching external controllers")
   114  	}
   115  	_ = w.catacomb.Add(watcher)
   116  
   117  	watchers := names.NewSet()
   118  	for {
   119  		select {
   120  		case <-w.catacomb.Dying():
   121  			return w.catacomb.ErrDying()
   122  
   123  		case ids, ok := <-watcher.Changes():
   124  			if !ok {
   125  				return w.catacomb.ErrDying()
   126  			}
   127  
   128  			if len(ids) == 0 {
   129  				continue
   130  			}
   131  
   132  			logger.Debugf("external controllers changed: %q", ids)
   133  			tags := make([]names.ControllerTag, len(ids))
   134  			for i, id := range ids {
   135  				if !names.IsValidController(id) {
   136  					return errors.Errorf("%q is not a valid controller tag", id)
   137  				}
   138  				tags[i] = names.NewControllerTag(id)
   139  			}
   140  
   141  			for _, tag := range tags {
   142  				// We're informed when an external controller
   143  				// is added or removed, so treat as a toggle.
   144  				if watchers.Contains(tag) {
   145  					logger.Infof("stopping watcher for external controller %q", tag.Id())
   146  					_ = w.runner.StopAndRemoveWorker(tag.Id(), w.catacomb.Dying())
   147  					watchers.Remove(tag)
   148  					continue
   149  				}
   150  				logger.Infof("starting watcher for external controller %q", tag.Id())
   151  				watchers.Add(tag)
   152  				if err := w.runner.StartWorker(tag.Id(), func() (worker.Worker, error) {
   153  					cw := controllerWatcher{
   154  						tag:                                tag,
   155  						setExternalControllerInfo:          w.setExternalControllerInfo,
   156  						externalControllerInfo:             w.externalControllerInfo,
   157  						newExternalControllerWatcherClient: w.newExternalControllerWatcherClient,
   158  					}
   159  					if err := catacomb.Invoke(catacomb.Plan{
   160  						Site: &cw.catacomb,
   161  						Work: cw.loop,
   162  					}); err != nil {
   163  						return nil, errors.Trace(err)
   164  					}
   165  					return &cw, nil
   166  				}); err != nil {
   167  					return errors.Annotatef(err, "starting watcher for external controller %q", tag.Id())
   168  				}
   169  			}
   170  		}
   171  	}
   172  }
   173  
   174  // controllerWatcher is a worker that watches for changes to the external
   175  // controller with the given tag. The external controller must be known
   176  // to the local controller.
   177  type controllerWatcher struct {
   178  	catacomb catacomb.Catacomb
   179  
   180  	tag                                names.ControllerTag
   181  	setExternalControllerInfo          func(crossmodel.ControllerInfo) error
   182  	externalControllerInfo             func(controllerUUID string) (*crossmodel.ControllerInfo, error)
   183  	newExternalControllerWatcherClient NewExternalControllerWatcherClientFunc
   184  }
   185  
   186  // Kill is part of the worker.Worker interface.
   187  func (w *controllerWatcher) Kill() {
   188  	w.catacomb.Kill(nil)
   189  }
   190  
   191  // Wait is part of the worker.Worker interface.
   192  func (w *controllerWatcher) Wait() error {
   193  	err := w.catacomb.Wait()
   194  	if errors.Cause(err) == rpc.ErrShutdown {
   195  		// RPC shutdown errors need to be ignored.
   196  		return nil
   197  	}
   198  	return err
   199  }
   200  
   201  func (w *controllerWatcher) loop() error {
   202  	// We get the API info from the local controller initially.
   203  	info, err := w.externalControllerInfo(w.tag.Id())
   204  	if errors.IsNotFound(err) {
   205  		return nil
   206  	} else if err != nil {
   207  		return errors.Annotate(err, "getting cached external controller info")
   208  	}
   209  	logger.Debugf("controller info for controller %q: %v", w.tag.Id(), info)
   210  
   211  	var nw watcher.NotifyWatcher
   212  	var client ExternalControllerWatcherClientCloser
   213  	defer func() {
   214  		if client != nil {
   215  			_ = client.Close()
   216  		}
   217  	}()
   218  
   219  	for {
   220  		if client == nil {
   221  			apiInfo := &api.Info{
   222  				Addrs:  info.Addrs,
   223  				CACert: info.CACert,
   224  				Tag:    names.NewUserTag(api.AnonymousUsername),
   225  			}
   226  			client, nw, err = w.connectAndWatch(apiInfo, w.catacomb.Dying())
   227  			if err == w.catacomb.ErrDying() {
   228  				return err
   229  			} else if err != nil {
   230  				return errors.Trace(err)
   231  			}
   232  			_ = w.catacomb.Add(nw)
   233  		}
   234  
   235  		select {
   236  		case <-w.catacomb.Dying():
   237  			return w.catacomb.ErrDying()
   238  		case _, ok := <-nw.Changes():
   239  			if !ok {
   240  				return w.catacomb.ErrDying()
   241  			}
   242  
   243  			newInfo, err := client.ControllerInfo()
   244  			if err != nil {
   245  				return errors.Annotate(err, "getting external controller info")
   246  			}
   247  			if reflect.DeepEqual(newInfo.Addrs, info.Addrs) {
   248  				continue
   249  			}
   250  			// API addresses have changed. Save the details to the
   251  			// local controller and stop the existing notify watcher
   252  			// and set it to nil, so we'll restart it with the new
   253  			// addresses.
   254  			info.Addrs = newInfo.Addrs
   255  			if err := w.setExternalControllerInfo(crossmodel.ControllerInfo{
   256  				ControllerTag: w.tag,
   257  				Alias:         info.Alias,
   258  				Addrs:         info.Addrs,
   259  				CACert:        info.CACert,
   260  			}); err != nil {
   261  				return errors.Annotate(err, "caching external controller info")
   262  			}
   263  			logger.Infof("new controller info for controller %q: %v", w.tag.Id(), info)
   264  			if err := worker.Stop(nw); err != nil {
   265  				return errors.Trace(err)
   266  			}
   267  			if err := client.Close(); err != nil {
   268  				return errors.Trace(err)
   269  			}
   270  			client = nil
   271  			nw = nil
   272  		}
   273  	}
   274  }
   275  
   276  // connectAndWatch connects to the specified controller and watches for changes.
   277  // It aborts if signalled, which prevents the watcher loop from blocking any shutdown
   278  // of the watcher the may be requested by the parent worker.
   279  func (w *controllerWatcher) connectAndWatch(apiInfo *api.Info, abort <-chan struct{}) (ExternalControllerWatcherClientCloser, watcher.NotifyWatcher, error) {
   280  	type result struct {
   281  		client ExternalControllerWatcherClientCloser
   282  		nw     watcher.NotifyWatcher
   283  	}
   284  	donec := make(chan result, 1)
   285  	errc := make(chan error, 1)
   286  	go func() {
   287  		client, err := w.newExternalControllerWatcherClient(apiInfo)
   288  		if err != nil {
   289  			errc <- errors.Annotate(err, "getting external controller client")
   290  			return
   291  		}
   292  		nw, err := client.WatchControllerInfo()
   293  		if err != nil {
   294  			errc <- errors.Annotate(err, "watching external controller")
   295  			return
   296  		}
   297  		donec <- result{client, nw}
   298  	}()
   299  	select {
   300  	case <-abort:
   301  		return nil, nil, w.catacomb.ErrDying()
   302  	case err := <-errc:
   303  		return nil, nil, errors.Trace(err)
   304  	case r := <-donec:
   305  		return r.client, r.nw, nil
   306  	}
   307  }