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

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package caasoperator
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/names/v5"
    17  	jujusymlink "github.com/juju/utils/v3/symlink"
    18  	"github.com/juju/version/v2"
    19  	"github.com/juju/worker/v3"
    20  	"github.com/juju/worker/v3/catacomb"
    21  
    22  	apiuniter "github.com/juju/juju/api/agent/uniter"
    23  	"github.com/juju/juju/caas"
    24  	caasconstants "github.com/juju/juju/caas/kubernetes/provider/constants"
    25  	"github.com/juju/juju/caas/kubernetes/provider/exec"
    26  	"github.com/juju/juju/core/arch"
    27  	"github.com/juju/juju/core/leadership"
    28  	"github.com/juju/juju/core/life"
    29  	"github.com/juju/juju/core/model"
    30  	coreos "github.com/juju/juju/core/os"
    31  	"github.com/juju/juju/core/paths"
    32  	"github.com/juju/juju/core/status"
    33  	"github.com/juju/juju/core/watcher"
    34  	jujunames "github.com/juju/juju/juju/names"
    35  	"github.com/juju/juju/juju/sockets"
    36  	jujuversion "github.com/juju/juju/version"
    37  	jworker "github.com/juju/juju/worker"
    38  	"github.com/juju/juju/worker/caasoperator/remotestate"
    39  	"github.com/juju/juju/worker/introspection"
    40  	"github.com/juju/juju/worker/uniter"
    41  	jujucharm "github.com/juju/juju/worker/uniter/charm"
    42  	uniterremotestate "github.com/juju/juju/worker/uniter/remotestate"
    43  	"github.com/juju/juju/wrench"
    44  )
    45  
    46  // logger is here to stop the desire of creating a package level logger.
    47  // Don't do this, instead pass one through as config to the worker.
    48  type logger interface{}
    49  
    50  var _ logger = struct{}{}
    51  
    52  var (
    53  	jujuExec       = paths.JujuExec(paths.CurrentOS())
    54  	jujuDumpLogs   = paths.JujuDumpLogs(paths.CurrentOS())
    55  	jujuIntrospect = paths.JujuIntrospect(paths.CurrentOS())
    56  
    57  	jujudSymlinks = []string{
    58  		jujuExec,
    59  		jujuDumpLogs,
    60  		jujuIntrospect,
    61  	}
    62  )
    63  
    64  // caasOperator implements the capabilities of the caasoperator agent. It is not intended to
    65  // implement the actual *behaviour* of the caasoperator agent; that responsibility is
    66  // delegated to Mode values, which are expected to react to events and direct
    67  // the caasoperator's responses to them.
    68  type caasOperator struct {
    69  	catacomb       catacomb.Catacomb
    70  	config         Config
    71  	paths          Paths
    72  	runner         *worker.Runner
    73  	deployer       jujucharm.Deployer
    74  	stateFile      *StateFile
    75  	deploymentMode caas.DeploymentMode
    76  }
    77  
    78  // Config hold the configuration for a caasoperator worker.
    79  type Config struct {
    80  	Logger Logger
    81  
    82  	// ModelUUID is the UUID of the model.
    83  	ModelUUID string
    84  
    85  	// ModelName is the name of the model.
    86  	ModelName string
    87  
    88  	// Application holds the name of the application that
    89  	// this CAAS operator manages.
    90  	Application string
    91  
    92  	// CharmGetter is an interface used for getting the
    93  	// application's charm URL and SHA256 hash.
    94  	CharmGetter CharmGetter
    95  
    96  	// Clock holds the clock to be used by the CAAS operator
    97  	// for time-related operations.
    98  	Clock clock.Clock
    99  
   100  	// DataDir holds the path to the Juju "data directory",
   101  	// i.e. "/var/lib/juju" (by default). The CAAS operator
   102  	// expects to find the jujud binary at <data-dir>/tools/jujud.
   103  	DataDir string
   104  
   105  	// ProfileDir is where the introspection scripts are written.
   106  	ProfileDir string
   107  
   108  	// Downloader is an interface used for downloading the
   109  	// application charm.
   110  	Downloader Downloader
   111  
   112  	// StatusSetter is an interface used for setting the
   113  	// application status.
   114  	StatusSetter StatusSetter
   115  
   116  	// UnitGetter is an interface for getting a unit.
   117  	UnitGetter UnitGetter
   118  
   119  	// UnitRemover is an interface for removing a unit.
   120  	UnitRemover UnitRemover
   121  
   122  	// ApplicationWatcher is an interface for getting info about an application's charm.
   123  	ApplicationWatcher ApplicationWatcher
   124  
   125  	// ContainerStartWatcher provides an interface for watching
   126  	// for unit container starts.
   127  	ContainerStartWatcher ContainerStartWatcher
   128  
   129  	// VersionSetter is an interface for setting the operator agent version.
   130  	VersionSetter VersionSetter
   131  
   132  	// LeadershipTrackerFunc is a function for getting a leadership tracker worker.
   133  	LeadershipTrackerFunc func(unitTag names.UnitTag) leadership.TrackerWorker
   134  
   135  	// UniterFacadeFunc is a function for making a uniter facade.
   136  	UniterFacadeFunc func(unitTag names.UnitTag) *apiuniter.State
   137  
   138  	// ResourcesFacadeFunc is a function for making a unit resources facade.
   139  	ResourcesFacadeFunc func(unitTag names.UnitTag) (*apiuniter.ResourcesFacadeClient, error)
   140  
   141  	// PayloadFacadeFunc is a function for making a unit payload facade.
   142  	PayloadFacadeFunc func() *apiuniter.PayloadFacadeClient
   143  
   144  	// UniterParams are parameters used to construct a uniter worker.
   145  	UniterParams *uniter.UniterParams
   146  
   147  	// StartUniterFunc starts a uniter worker using the given runner.
   148  	StartUniterFunc func(runner *worker.Runner, params *uniter.UniterParams) error
   149  
   150  	// RunListenerSocketFunc returns a socket used for the juju run listener.
   151  	RunListenerSocketFunc func(*uniter.SocketConfig) (*sockets.Socket, error)
   152  
   153  	// OperatorInfo contains serving information such as Certs and PrivateKeys.
   154  	OperatorInfo caas.OperatorInfo
   155  
   156  	// ExecClientGetter returns an exec client for initializing caas units.
   157  	ExecClientGetter func() (exec.Executor, error)
   158  }
   159  
   160  func (config Config) Validate() error {
   161  	if !names.IsValidApplication(config.Application) {
   162  		return errors.NotValidf("application name %q", config.Application)
   163  	}
   164  	if config.CharmGetter == nil {
   165  		return errors.NotValidf("missing CharmGetter")
   166  	}
   167  	if config.ApplicationWatcher == nil {
   168  		return errors.NotValidf("missing ApplicationWatcher")
   169  	}
   170  	if config.UnitGetter == nil {
   171  		return errors.NotValidf("missing UnitGetter")
   172  	}
   173  	if config.UnitRemover == nil {
   174  		return errors.NotValidf("missing UnitRemover")
   175  	}
   176  	if config.ContainerStartWatcher == nil {
   177  		return errors.NotValidf("missing ContainerStartWatcher")
   178  	}
   179  	if config.LeadershipTrackerFunc == nil {
   180  		return errors.NotValidf("missing LeadershipTrackerFunc")
   181  	}
   182  	if config.UniterFacadeFunc == nil {
   183  		return errors.NotValidf("missing UniterFacadeFunc")
   184  	}
   185  	if config.ResourcesFacadeFunc == nil {
   186  		return errors.NotValidf("missing ResourcesFacadeFunc")
   187  	}
   188  	if config.PayloadFacadeFunc == nil {
   189  		return errors.NotValidf("missing PayloadFacadeFunc")
   190  	}
   191  	if config.UniterParams == nil {
   192  		return errors.NotValidf("missing UniterParams")
   193  	}
   194  	if config.Clock == nil {
   195  		return errors.NotValidf("missing Clock")
   196  	}
   197  	if config.DataDir == "" {
   198  		return errors.NotValidf("missing DataDir")
   199  	}
   200  	if config.ProfileDir == "" {
   201  		return errors.NotValidf("missing ProfileDir")
   202  	}
   203  	if config.Downloader == nil {
   204  		return errors.NotValidf("missing Downloader")
   205  	}
   206  	if config.StatusSetter == nil {
   207  		return errors.NotValidf("missing StatusSetter")
   208  	}
   209  	if config.VersionSetter == nil {
   210  		return errors.NotValidf("missing VersionSetter")
   211  	}
   212  	if config.ExecClientGetter == nil {
   213  		return errors.NotValidf("missing ExecClientGetter")
   214  	}
   215  
   216  	if config.Logger == nil {
   217  		return errors.NotValidf("missing Logger")
   218  	}
   219  	return nil
   220  }
   221  
   222  func (config Config) getPaths() Paths {
   223  	return NewPaths(config.DataDir, names.NewApplicationTag(config.Application))
   224  }
   225  
   226  // NewWorker creates a new worker which will install and operate a
   227  // CaaS-based application, by executing hooks and operations in
   228  // response to application state changes.
   229  func NewWorker(config Config) (worker.Worker, error) {
   230  	if err := config.Validate(); err != nil {
   231  		return nil, errors.Trace(err)
   232  	}
   233  	paths := config.getPaths()
   234  	logger := config.Logger.Child("charm")
   235  	deployer, err := jujucharm.NewDeployer(
   236  		paths.State.CharmDir,
   237  		paths.State.DeployerDir,
   238  		jujucharm.NewBundlesDir(
   239  			paths.State.BundlesDir,
   240  			config.Downloader,
   241  			logger),
   242  		logger,
   243  	)
   244  	if err != nil {
   245  		return nil, errors.Annotatef(err, "cannot create deployer")
   246  	}
   247  
   248  	op := &caasOperator{
   249  		config:   config,
   250  		paths:    paths,
   251  		deployer: deployer,
   252  		runner: worker.NewRunner(worker.RunnerParams{
   253  			Clock: config.Clock,
   254  
   255  			// One of the uniter workers failing should not
   256  			// prevent the others from running.
   257  			IsFatal: func(error) bool { return false },
   258  
   259  			// For any failures, try again in 3 seconds.
   260  			RestartDelay: 3 * time.Second,
   261  			Logger:       config.Logger.Child("runner"),
   262  		}),
   263  	}
   264  	if err := catacomb.Invoke(catacomb.Plan{
   265  		Site: &op.catacomb,
   266  		Work: op.loop,
   267  		Init: []worker.Worker{op.runner},
   268  	}); err != nil {
   269  		return nil, errors.Trace(err)
   270  	}
   271  	return op, nil
   272  }
   273  
   274  func (op *caasOperator) makeAgentSymlinks(unitTag names.UnitTag) error {
   275  	// All units share the same agent binary.
   276  	// Set up the required symlinks.
   277  
   278  	// First the agent binary.
   279  	agentBinaryDir := op.paths.GetToolsDir()
   280  	unitToolsDir := filepath.Join(agentBinaryDir, unitTag.String())
   281  	err := os.Mkdir(unitToolsDir, 0600)
   282  	if err != nil && !os.IsExist(err) {
   283  		return errors.Trace(err)
   284  	}
   285  	jujudPath := filepath.Join(agentBinaryDir, jujunames.Jujud)
   286  	err = jujusymlink.New(jujudPath, filepath.Join(unitToolsDir, jujunames.Jujud))
   287  	// Ignore permission denied as this won't happen in production
   288  	// but may happen in testing depending on setup of /tmp
   289  	if err != nil && !os.IsExist(err) && !os.IsPermission(err) {
   290  		return errors.Trace(err)
   291  	}
   292  
   293  	// TODO(caas) - remove this when upstream charmhelpers are fixed
   294  	// Charmhelpers expect to see a jujud in a machine-X directory.
   295  	legacyMachineDir := filepath.Join(agentBinaryDir, "machine-0")
   296  	err = os.Mkdir(legacyMachineDir, 0600)
   297  	if err != nil && !os.IsExist(err) {
   298  		return errors.Trace(err)
   299  	}
   300  	err = jujusymlink.New(jujudPath, filepath.Join(legacyMachineDir, jujunames.Jujud))
   301  	if err != nil && !os.IsExist(err) && !os.IsPermission(err) {
   302  		return errors.Trace(err)
   303  	}
   304  
   305  	for _, slk := range jujudSymlinks {
   306  		err = jujusymlink.New(jujudPath, slk)
   307  		if err != nil && !os.IsExist(err) && !os.IsPermission(err) {
   308  			return errors.Trace(err)
   309  		}
   310  		// TODO(juju 4) - remove this legacy behaviour.
   311  		// Remove the obsolete "juju-run" symlink
   312  		if strings.Contains(slk, "/juju-exec") {
   313  			runLink := strings.Replace(slk, "/juju-exec", "/juju-run", 1)
   314  			_ = os.Remove(runLink)
   315  		}
   316  	}
   317  
   318  	// Ensure legacy charm symlinks created before 2.8 getting unlinked.
   319  	unitCharmDir := filepath.Join(op.config.DataDir, "agents", unitTag.String(), "charm")
   320  	isUnitCharmDirSymlink, err := jujusymlink.IsSymlink(unitCharmDir)
   321  	if os.IsNotExist(errors.Cause(err)) || os.IsPermission(errors.Cause(err)) {
   322  		// Ignore permission denied as this won't happen in production
   323  		// but may happen in testing depending on setup of /tmp.
   324  		return nil
   325  	} else if err != nil {
   326  		return errors.Trace(err)
   327  	}
   328  	if isUnitCharmDirSymlink {
   329  		op.config.Logger.Warningf("removing legacy charm symlink for %q", unitTag.String())
   330  		if err := os.Remove(unitCharmDir); err != nil {
   331  			return errors.Trace(err)
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  func (op *caasOperator) removeUnitDir(unitTag names.UnitTag) error {
   338  	unitAgentDir := filepath.Join(op.config.DataDir, "agents", unitTag.String())
   339  	return os.RemoveAll(unitAgentDir)
   340  }
   341  
   342  func toBinaryVersion(vers version.Number, osType string) version.Binary {
   343  	outVers := version.Binary{
   344  		Number:  vers,
   345  		Arch:    arch.HostArch(),
   346  		Release: osType,
   347  	}
   348  	return outVers
   349  }
   350  
   351  func runListenerSocket(sc *uniter.SocketConfig) (*sockets.Socket, error) {
   352  	socket := sockets.Socket{
   353  		Network:   "tcp",
   354  		Address:   fmt.Sprintf(":%d", caasconstants.JujuExecServerSocketPort),
   355  		TLSConfig: sc.TLSConfig,
   356  	}
   357  	return &socket, nil
   358  }
   359  
   360  func (op *caasOperator) init() (*LocalState, error) {
   361  	if err := introspection.WriteProfileFunctions(op.config.ProfileDir); err != nil {
   362  		// This isn't fatal, just annoying.
   363  		op.config.Logger.Errorf("failed to write profile funcs: %v", err)
   364  	}
   365  
   366  	if err := jujucharm.ClearDownloads(op.paths.State.BundlesDir); err != nil {
   367  		op.config.Logger.Warningf(err.Error())
   368  	}
   369  
   370  	op.stateFile = NewStateFile(op.paths.State.OperationsFile)
   371  	localState, err := op.stateFile.Read()
   372  	if err == ErrNoStateFile {
   373  		localState = &LocalState{}
   374  	}
   375  
   376  	if err := op.ensureCharm(localState); err != nil {
   377  		if err == jworker.ErrTerminateAgent {
   378  			return nil, err
   379  		}
   380  		return nil, errors.Annotatef(err,
   381  			"failed to initialize caasoperator for %q",
   382  			op.config.Application,
   383  		)
   384  	}
   385  
   386  	// Set up a single remote juju run listener to be used by all workload units.
   387  	if op.deploymentMode != caas.ModeOperator {
   388  		if op.config.RunListenerSocketFunc == nil {
   389  			return nil, errors.New("missing RunListenerSocketFunc")
   390  		}
   391  		if op.config.RunListenerSocketFunc != nil {
   392  			socket, err := op.config.RunListenerSocketFunc(op.config.UniterParams.SocketConfig)
   393  			if err != nil {
   394  				return nil, errors.Annotate(err, "creating juju run socket")
   395  			}
   396  			op.config.Logger.Debugf("starting caas operator juju-exec listener on %v", socket)
   397  			logger := loggo.GetLogger("juju.worker.uniter")
   398  			runListener, err := uniter.NewRunListener(*socket, logger)
   399  			if err != nil {
   400  				return nil, errors.Annotate(err, "creating juju run listener")
   401  			}
   402  			rlw := uniter.NewRunListenerWrapper(runListener, logger)
   403  			if err := op.catacomb.Add(rlw); err != nil {
   404  				return nil, errors.Trace(err)
   405  			}
   406  			op.config.UniterParams.RunListener = runListener
   407  		}
   408  	}
   409  	return localState, nil
   410  }
   411  
   412  func (op *caasOperator) loop() (err error) {
   413  	logger := op.config.Logger
   414  
   415  	defer func() {
   416  		if err == nil {
   417  			logger.Debugf("operator %q is peacefully shutting down", op.config.Application)
   418  		} else {
   419  			logger.Warningf("operator %q is shutting down, err: %s", op.config.Application, err.Error())
   420  		}
   421  		if errors.IsNotFound(err) {
   422  			err = jworker.ErrTerminateAgent
   423  		}
   424  	}()
   425  
   426  	localState, err := op.init()
   427  	if err != nil {
   428  		return errors.Trace(err)
   429  	}
   430  	logger.Infof("operator %q started", op.config.Application)
   431  
   432  	// Start by reporting current tools (which includes arch/ostype).
   433  	hostOSType := coreos.HostOSTypeName()
   434  	if err := op.config.VersionSetter.SetVersion(
   435  		op.config.Application, toBinaryVersion(jujuversion.Current, hostOSType)); err != nil {
   436  		return errors.Annotate(err, "cannot set agent version")
   437  	}
   438  
   439  	var remoteWatcher remotestate.Watcher
   440  
   441  	restartWatcher := func() error {
   442  		if remoteWatcher != nil {
   443  			// watcher added to catacomb, will kill operator if there's an error.
   444  			_ = worker.Stop(remoteWatcher)
   445  		}
   446  		var err error
   447  		remoteWatcher, err = remotestate.NewWatcher(
   448  			remotestate.WatcherConfig{
   449  				Logger:             loggo.GetLogger("juju.worker.caasoperator.remotestate"),
   450  				CharmGetter:        op.config.CharmGetter,
   451  				Application:        op.config.Application,
   452  				ApplicationWatcher: op.config.ApplicationWatcher,
   453  			})
   454  		if err != nil {
   455  			return errors.Trace(err)
   456  		}
   457  		if err := op.catacomb.Add(remoteWatcher); err != nil {
   458  			return errors.Trace(err)
   459  		}
   460  		return nil
   461  	}
   462  
   463  	jujuUnitsWatcher, err := op.config.UnitGetter.WatchUnits(op.config.Application)
   464  	if err != nil {
   465  		return errors.Trace(err)
   466  	}
   467  	if err := op.catacomb.Add(jujuUnitsWatcher); err != nil {
   468  		return errors.Trace(err)
   469  	}
   470  
   471  	var containerStartChan watcher.StringsChannel
   472  	if op.deploymentMode != caas.ModeOperator {
   473  		// Match the init container and the default container.
   474  		containerRegex := fmt.Sprintf("(?:%s|)", caas.InitContainerName)
   475  		containerStartWatcher, err := op.config.ContainerStartWatcher.WatchContainerStart(
   476  			op.config.Application, containerRegex)
   477  		if err != nil {
   478  			return errors.Trace(err)
   479  		}
   480  		if err := op.catacomb.Add(containerStartWatcher); err != nil {
   481  			return errors.Trace(err)
   482  		}
   483  		containerStartChan = containerStartWatcher.Changes()
   484  	}
   485  
   486  	if err := op.setStatus(status.Active, ""); err != nil {
   487  		return errors.Trace(err)
   488  	}
   489  
   490  	// Channels used to notify uniter worker that the workload container
   491  	// is running.
   492  	unitRunningChannels := make(map[string]chan struct{})
   493  
   494  	if err = restartWatcher(); err != nil {
   495  		err = errors.Annotate(err, "(re)starting watcher")
   496  		return errors.Trace(err)
   497  	}
   498  
   499  	// We should not do anything until there has been a change
   500  	// to the remote state. The watcher will trigger at least
   501  	// once initially.
   502  	select {
   503  	case <-op.catacomb.Dying():
   504  		return op.catacomb.ErrDying()
   505  	case <-remoteWatcher.RemoteStateChanged():
   506  	}
   507  
   508  	for {
   509  		select {
   510  		case <-op.catacomb.Dying():
   511  			return op.catacomb.ErrDying()
   512  		case <-remoteWatcher.RemoteStateChanged():
   513  			snap := remoteWatcher.Snapshot()
   514  			if op.charmModified(localState, snap) {
   515  				// Charm changed so download and install the new version.
   516  				err := op.ensureCharm(localState)
   517  				if err != nil {
   518  					return errors.Annotatef(err, "error downloading updated charm %v", localState.CharmURL)
   519  				}
   520  				// Reset the application's "Downloading..." message.
   521  				if err := op.setStatus(status.Active, ""); err != nil {
   522  					return errors.Trace(err)
   523  				}
   524  			}
   525  		case units, ok := <-containerStartChan:
   526  			if !ok {
   527  				return errors.New("container start watcher closed channel")
   528  			}
   529  			for _, unitID := range units {
   530  				if runningChan, ok := unitRunningChannels[unitID]; ok {
   531  					logger.Debugf("trigger running status for caas unit %v", unitID)
   532  					select {
   533  					case <-op.catacomb.Dying():
   534  						return op.catacomb.ErrDying()
   535  					case runningChan <- struct{}{}:
   536  					default:
   537  						// This will happen when the buffered channel already
   538  						// has an event. If this is the case it's ok to discard
   539  						// the event.
   540  						logger.Debugf("unit running chan[%q] discarding running event as one already exists", unitID)
   541  					}
   542  				}
   543  			}
   544  		case units, ok := <-jujuUnitsWatcher.Changes():
   545  			if !ok {
   546  				return errors.New("watcher closed channel")
   547  			}
   548  			for _, v := range units {
   549  				unitID := v
   550  				unitLife, err := op.config.UnitGetter.Life(unitID)
   551  				if err != nil && !errors.IsNotFound(err) {
   552  					return errors.Trace(err)
   553  				}
   554  				logger.Debugf("got unit change %q (%s)", unitID, unitLife)
   555  				unitTag := names.NewUnitTag(unitID)
   556  				if errors.IsNotFound(err) || unitLife == life.Dead {
   557  					delete(unitRunningChannels, unitID)
   558  					logger.Debugf("stopping uniter for dead unit %q", unitID)
   559  					if err := op.runner.StopAndRemoveWorker(unitID, op.catacomb.Dying()); err != nil {
   560  						logger.Warningf("stopping uniter for dead unit %q: %v", unitID, err)
   561  					}
   562  					logger.Debugf("removing dead unit %q", unitID)
   563  					// Remove the unit from state.
   564  					if err := op.config.UnitRemover.RemoveUnit(unitID); err != nil {
   565  						return errors.Trace(err)
   566  					}
   567  					logger.Debugf("removing unit dir for dead unit %q", unitID)
   568  					// Remove the unit's directory
   569  					if err := op.removeUnitDir(unitTag); err != nil {
   570  						return errors.Trace(err)
   571  					}
   572  					// Nothing to do for a dead unit further.
   573  					continue
   574  				} else {
   575  					if _, ok := unitRunningChannels[unitID]; !ok && op.deploymentMode != caas.ModeOperator {
   576  						// We make a buffered channel here so that we don't
   577  						// block the operator while the uniter may not be ready
   578  						unitRunningChannels[unitID] = make(chan struct{}, 1)
   579  					}
   580  				}
   581  				// Start a worker to manage any new units.
   582  				if _, err := op.runner.Worker(unitID, op.catacomb.Dying()); err == nil || unitLife == life.Dead {
   583  					// Already watching the unit or we're
   584  					// not yet watching it and it's dead.
   585  					continue
   586  				}
   587  
   588  				// Make all the required symlinks.
   589  				if err := op.makeAgentSymlinks(unitTag); err != nil {
   590  					return errors.Trace(err)
   591  				}
   592  				params := *op.config.UniterParams
   593  				params.ModelType = model.CAAS
   594  				params.UnitTag = unitTag
   595  				params.Downloader = op.config.Downloader // TODO(caas): write a cache downloader
   596  				params.UniterFacade = op.config.UniterFacadeFunc(unitTag)
   597  				if params.ResourcesFacade, err = op.config.ResourcesFacadeFunc(unitTag); err != nil {
   598  					return errors.Trace(err)
   599  				}
   600  				params.PayloadFacade = op.config.PayloadFacadeFunc()
   601  				params.LeadershipTrackerFunc = op.config.LeadershipTrackerFunc
   602  				params.Logger = params.Logger.Child(unitID)
   603  				if op.deploymentMode != caas.ModeOperator {
   604  					params.IsRemoteUnit = true
   605  					params.ContainerRunningStatusChannel = unitRunningChannels[unitID]
   606  
   607  					execClient, err := op.config.ExecClientGetter()
   608  					if err != nil {
   609  						return errors.Trace(err)
   610  					}
   611  					params.ContainerRunningStatusFunc = func(providerID string) (*uniterremotestate.ContainerRunningStatus, error) {
   612  						if wrench.IsActive("remote-init", "fatal-error") {
   613  							return nil, errors.New("fake remote-init fatal-error")
   614  						}
   615  						return op.runningStatus(execClient, unitTag, providerID)
   616  					}
   617  					params.RemoteInitFunc = func(runningStatus uniterremotestate.ContainerRunningStatus, cancel <-chan struct{}) error {
   618  						// TODO(caas): Remove the cached status uniterremotestate.ContainerRunningStatus from uniter watcher snapshot.
   619  						return op.remoteInitForUniter(execClient, unitTag, runningStatus, cancel)
   620  					}
   621  					params.NewRemoteRunnerExecutor = getNewRunnerExecutor(logger, execClient)
   622  				}
   623  				if err := op.config.StartUniterFunc(op.runner, &params); err != nil {
   624  					return errors.Trace(err)
   625  				}
   626  			}
   627  		}
   628  	}
   629  }
   630  
   631  func (op *caasOperator) runningStatus(client exec.Executor, unit names.UnitTag, providerID string) (*uniterremotestate.ContainerRunningStatus, error) {
   632  	op.config.Logger.Debugf("request running status for %q %s", unit.String(), providerID)
   633  	params := exec.StatusParams{
   634  		PodName: providerID,
   635  	}
   636  	status, err := client.Status(params)
   637  	if err != nil {
   638  		op.config.Logger.Errorf("could not get pod %q %q %v", unit.String(), providerID, err)
   639  		return nil, errors.Annotatef(err, "getting pod status for unit %q, container %q", unit, providerID)
   640  	}
   641  	result := &uniterremotestate.ContainerRunningStatus{
   642  		PodName: status.PodName,
   643  	}
   644  	once := true
   645  	for _, cs := range status.ContainerStatus {
   646  		if cs.InitContainer && cs.Name == caas.InitContainerName {
   647  			result.Initialising = cs.Running
   648  			result.InitialisingTime = cs.StartedAt
   649  		}
   650  		// Only check the default container.
   651  		if !cs.InitContainer && !cs.EphemeralContainer && once {
   652  			result.Running = cs.Running
   653  			once = false
   654  		}
   655  	}
   656  	return result, nil
   657  }
   658  func (op *caasOperator) remoteInitForUniter(client exec.Executor, unit names.UnitTag, runningStatus uniterremotestate.ContainerRunningStatus, cancel <-chan struct{}) error {
   659  	return runnerWithRetry(
   660  		func() error {
   661  			status, err := op.runningStatus(client, unit, runningStatus.PodName)
   662  			//  get the current status rather than using the status cached in remote state.
   663  			if err != nil {
   664  				return errors.Trace(err)
   665  			}
   666  			return op.remoteInit(client, unit, *status, cancel)
   667  		},
   668  		func(err error) bool {
   669  			// We need to re-fetch the running status then retry remoteInit if the container is not running.
   670  			return err != nil && !exec.IsContainerNotRunningError(err) && !errors.IsNotFound(err)
   671  		}, op.config.Logger, op.config.Clock, cancel,
   672  	)
   673  }
   674  
   675  func (op *caasOperator) remoteInit(client exec.Executor, unit names.UnitTag, runningStatus uniterremotestate.ContainerRunningStatus, cancel <-chan struct{}) error {
   676  	op.config.Logger.Debugf("remote init for %q %+v", unit.String(), runningStatus)
   677  	switch {
   678  	case runningStatus.Initialising:
   679  		// all good, continue to do remote-init.
   680  		return errors.Trace(initializeUnit(initializeUnitParams{
   681  			ExecClient:   client,
   682  			Logger:       op.config.Logger,
   683  			OperatorInfo: op.config.OperatorInfo,
   684  			Paths:        op.paths,
   685  			UnitTag:      unit,
   686  			ProviderID:   runningStatus.PodName,
   687  			WriteFile:    os.WriteFile,
   688  			TempDir:      os.MkdirTemp,
   689  			Clock:        op.config.Clock,
   690  			ReTrier:      runnerWithRetry,
   691  		}, cancel))
   692  	case runningStatus.Running:
   693  		op.config.Logger.Debugf("no need to do remote-init for a running container")
   694  		return nil
   695  	default:
   696  		return errors.NotFoundf("container not running")
   697  	}
   698  }
   699  
   700  func (op *caasOperator) charmModified(local *LocalState, remote remotestate.Snapshot) bool {
   701  	// CAAS models may not yet have read the charm url from state.
   702  	if remote.CharmURL == nil {
   703  		return false
   704  	}
   705  	if local == nil || local.CharmURL == nil {
   706  		op.config.Logger.Warningf("unexpected nil local charm URL")
   707  		return true
   708  	}
   709  	if *local.CharmURL != *remote.CharmURL {
   710  		op.config.Logger.Debugf("upgrade from %v to %v", local.CharmURL, remote.CharmURL)
   711  		return true
   712  	}
   713  
   714  	if local.CharmModifiedVersion != remote.CharmModifiedVersion {
   715  		op.config.Logger.Debugf("upgrade from CharmModifiedVersion %v to %v", local.CharmModifiedVersion, remote.CharmModifiedVersion)
   716  		return true
   717  	}
   718  	if remote.ForceCharmUpgrade {
   719  		op.config.Logger.Debugf("force charm upgrade to %v", remote.CharmURL)
   720  		return true
   721  	}
   722  	return false
   723  }
   724  
   725  func (op *caasOperator) setStatus(status status.Status, message string, args ...interface{}) error {
   726  	err := op.config.StatusSetter.SetStatus(
   727  		op.config.Application,
   728  		status,
   729  		fmt.Sprintf(message, args...),
   730  		nil,
   731  	)
   732  	return errors.Annotate(err, "setting status")
   733  }
   734  
   735  // Kill is part of the worker.Worker interface.
   736  func (op *caasOperator) Kill() {
   737  	op.catacomb.Kill(nil)
   738  }
   739  
   740  // Wait is part of the worker.Worker interface.
   741  func (op *caasOperator) Wait() error {
   742  	return op.catacomb.Wait()
   743  }