github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/caasmodelconfigmanager/worker.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package caasmodelconfigmanager
     5  
     6  import (
     7  	"reflect"
     8  	"time"
     9  
    10  	"github.com/juju/clock"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/names/v5"
    14  	"github.com/juju/worker/v3"
    15  	"github.com/juju/worker/v3/catacomb"
    16  
    17  	"github.com/juju/juju/api/base"
    18  	api "github.com/juju/juju/api/controller/caasmodelconfigmanager"
    19  	"github.com/juju/juju/controller"
    20  	"github.com/juju/juju/core/watcher"
    21  	"github.com/juju/juju/docker"
    22  	"github.com/juju/juju/docker/registry"
    23  )
    24  
    25  const (
    26  	retryDuration   = 1 * time.Second
    27  	refreshDuration = 30 * time.Second
    28  )
    29  
    30  // Logger represents the methods used by the worker to log details.
    31  type Logger interface {
    32  	Debugf(string, ...interface{})
    33  	Infof(string, ...interface{})
    34  	Errorf(string, ...interface{})
    35  	Warningf(string, ...interface{})
    36  	Tracef(string, ...interface{})
    37  
    38  	Child(string) loggo.Logger
    39  }
    40  
    41  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/facade_mock.go github.com/juju/juju/worker/caasmodelconfigmanager Facade
    42  type Facade interface {
    43  	ControllerConfig() (controller.Config, error)
    44  	WatchControllerConfig() (watcher.NotifyWatcher, error)
    45  }
    46  
    47  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/broker_mock.go github.com/juju/juju/worker/caasmodelconfigmanager CAASBroker
    48  type CAASBroker interface {
    49  	EnsureImageRepoSecret(docker.ImageRepoDetails) error
    50  }
    51  
    52  // Config holds the configuration and dependencies for a worker.
    53  type Config struct {
    54  	ModelTag names.ModelTag
    55  
    56  	Facade       Facade
    57  	Broker       CAASBroker
    58  	Logger       Logger
    59  	Clock        clock.Clock
    60  	RegistryFunc func(docker.ImageRepoDetails) (registry.Registry, error)
    61  }
    62  
    63  // Validate returns an error if the config cannot be expected
    64  // to drive a functional worker.
    65  func (config Config) Validate() error {
    66  	if config.ModelTag == (names.ModelTag{}) {
    67  		return errors.NotValidf("ModelTag is missing")
    68  	}
    69  	if config.Facade == nil {
    70  		return errors.NotValidf("Facade is missing")
    71  	}
    72  	if config.Broker == nil {
    73  		return errors.NotValidf("Broker is missing")
    74  	}
    75  	if config.Logger == nil {
    76  		return errors.NotValidf("Logger is missing")
    77  	}
    78  	if config.Clock == nil {
    79  		return errors.NotValidf("Clock is missing")
    80  	}
    81  	if config.RegistryFunc == nil {
    82  		return errors.NotValidf("RegistryFunc is missing")
    83  	}
    84  	return nil
    85  }
    86  
    87  type manager struct {
    88  	catacomb catacomb.Catacomb
    89  
    90  	name   string
    91  	config Config
    92  	logger Logger
    93  	clock  clock.Clock
    94  
    95  	registryFunc func(docker.ImageRepoDetails) (registry.Registry, error)
    96  }
    97  
    98  // NewFacade returns a facade for caasapplicationprovisioner worker to use.
    99  func NewFacade(caller base.APICaller) (Facade, error) {
   100  	return api.NewClient(caller)
   101  }
   102  
   103  // NewWorker returns a worker that unlocks the model upgrade gate.
   104  func NewWorker(config Config) (worker.Worker, error) {
   105  	if err := config.Validate(); err != nil {
   106  		return nil, errors.Trace(err)
   107  	}
   108  	w := &manager{
   109  		name:         config.ModelTag.Id(),
   110  		config:       config,
   111  		logger:       config.Logger,
   112  		clock:        config.Clock,
   113  		registryFunc: config.RegistryFunc,
   114  	}
   115  	err := catacomb.Invoke(catacomb.Plan{
   116  		Site: &w.catacomb,
   117  		Work: w.loop,
   118  	})
   119  	if err != nil {
   120  		return nil, errors.Trace(err)
   121  	}
   122  	return w, nil
   123  }
   124  
   125  // Kill is part of the worker.Worker interface.
   126  func (w *manager) Kill() {
   127  	w.catacomb.Kill(nil)
   128  }
   129  
   130  // Wait is part of the worker.Worker interface.
   131  func (w *manager) Wait() error {
   132  	return w.catacomb.Wait()
   133  }
   134  
   135  func (w *manager) loop() (err error) {
   136  	watcher, err := w.config.Facade.WatchControllerConfig()
   137  	if err != nil {
   138  		return errors.Trace(err)
   139  	}
   140  	err = w.catacomb.Add(watcher)
   141  	if err != nil {
   142  		return errors.Trace(err)
   143  	}
   144  
   145  	var (
   146  		refresh         <-chan struct{}
   147  		timeout         <-chan time.Time
   148  		deadline        time.Time
   149  		reg             registry.Registry
   150  		lastRepoDetails docker.ImageRepoDetails
   151  	)
   152  	first := false
   153  	signal := make(chan struct{})
   154  	close(signal)
   155  	defer func() {
   156  		if reg != nil {
   157  			_ = reg.Close()
   158  		}
   159  	}()
   160  
   161  	for {
   162  		select {
   163  		case <-w.catacomb.Dying():
   164  			return w.catacomb.ErrDying()
   165  		case <-watcher.Changes():
   166  			controllerConfig, err := w.config.Facade.ControllerConfig()
   167  			if err != nil {
   168  				return errors.Trace(err)
   169  			}
   170  			repoDetails, err := docker.NewImageRepoDetails(controllerConfig.CAASImageRepo())
   171  			if err != nil {
   172  				return errors.Annotatef(err, "parsing %s", controller.CAASImageRepo)
   173  			}
   174  			if reflect.DeepEqual(repoDetails, lastRepoDetails) {
   175  				continue
   176  			}
   177  			lastRepoDetails = repoDetails
   178  			if !repoDetails.IsPrivate() {
   179  				timeout = nil
   180  				refresh = nil
   181  				continue
   182  			}
   183  			if reg != nil {
   184  				_ = reg.Close()
   185  			}
   186  			reg, err = w.registryFunc(repoDetails)
   187  			if err != nil {
   188  				return errors.Trace(err)
   189  			}
   190  			if err = reg.Ping(); err != nil {
   191  				return errors.Trace(err)
   192  			}
   193  			first = true
   194  			refresh = signal
   195  		case <-timeout:
   196  			timeout = nil
   197  			if refresh == nil {
   198  				refresh = signal
   199  			}
   200  		case <-refresh:
   201  			refresh = nil
   202  			next, err := w.ensureImageRepoSecret(reg, first)
   203  			if err != nil {
   204  				w.logger.Errorf("failed to update repository secret: %s", err.Error())
   205  				next = retryDuration
   206  			} else {
   207  				first = false
   208  			}
   209  			if nextDeadline := w.clock.Now().Add(next); timeout == nil || nextDeadline.Before(deadline) {
   210  				deadline = nextDeadline
   211  				timeout = w.clock.After(next)
   212  			}
   213  		}
   214  	}
   215  }
   216  
   217  func (w *manager) ensureImageRepoSecret(reg registry.Registry, force bool) (time.Duration, error) {
   218  	shouldRefresh, nextRefresh := reg.ShouldRefreshAuth()
   219  	if nextRefresh == time.Duration(0) {
   220  		nextRefresh = refreshDuration
   221  	}
   222  	if !shouldRefresh && !force {
   223  		return nextRefresh, nil
   224  	}
   225  
   226  	w.logger.Debugf("refreshing auth token for %q", w.name)
   227  	if err := reg.RefreshAuth(); err != nil {
   228  		return time.Duration(0), errors.Annotatef(err, "refreshing registry auth token for %q", w.name)
   229  	}
   230  
   231  	w.logger.Debugf("applying refreshed auth token for %q", w.name)
   232  	err := w.config.Broker.EnsureImageRepoSecret(reg.ImageRepoDetails())
   233  	if err != nil {
   234  		return time.Duration(0), errors.Annotatef(err, "ensuring image repository secret for %q", w.name)
   235  	}
   236  	return nextRefresh, nil
   237  }