github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/caasoperator/manifold.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  	"crypto/tls"
     8  	"encoding/pem"
     9  	"os"
    10  	"path"
    11  	"time"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/http/v2"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/names/v5"
    18  	"github.com/juju/worker/v3"
    19  	"github.com/juju/worker/v3/dependency"
    20  
    21  	"github.com/juju/juju/agent"
    22  	apileadership "github.com/juju/juju/api/agent/leadership"
    23  	"github.com/juju/juju/api/agent/secretsmanager"
    24  	apiuniter "github.com/juju/juju/api/agent/uniter"
    25  	"github.com/juju/juju/api/base"
    26  	"github.com/juju/juju/caas"
    27  	caasconstants "github.com/juju/juju/caas/kubernetes/provider/constants"
    28  	"github.com/juju/juju/caas/kubernetes/provider/exec"
    29  	coreleadership "github.com/juju/juju/core/leadership"
    30  	"github.com/juju/juju/core/machinelock"
    31  	"github.com/juju/juju/juju/sockets"
    32  	"github.com/juju/juju/rpc/params"
    33  	"github.com/juju/juju/secrets"
    34  	"github.com/juju/juju/worker/fortress"
    35  	"github.com/juju/juju/worker/leadership"
    36  	"github.com/juju/juju/worker/secretexpire"
    37  	"github.com/juju/juju/worker/secretrotate"
    38  	"github.com/juju/juju/worker/uniter"
    39  	"github.com/juju/juju/worker/uniter/charm"
    40  	"github.com/juju/juju/worker/uniter/operation"
    41  	"github.com/juju/juju/worker/uniter/runner"
    42  )
    43  
    44  type Logger interface {
    45  	Debugf(string, ...interface{})
    46  	Infof(string, ...interface{})
    47  	Errorf(string, ...interface{})
    48  	Warningf(string, ...interface{})
    49  
    50  	Child(string) loggo.Logger
    51  }
    52  
    53  // ManifoldConfig defines the names of the manifolds on which a
    54  // Manifold will depend.
    55  type ManifoldConfig struct {
    56  	Logger Logger
    57  
    58  	AgentName     string
    59  	APICallerName string
    60  	ClockName     string
    61  
    62  	MachineLock           machinelock.Lock
    63  	LeadershipGuarantee   time.Duration
    64  	CharmDirName          string
    65  	ProfileDir            string
    66  	HookRetryStrategyName string
    67  	TranslateResolverErr  func(error) error
    68  
    69  	NewWorker          func(Config) (worker.Worker, error)
    70  	NewClient          func(base.APICaller) Client
    71  	NewCharmDownloader func(base.APICaller) Downloader
    72  
    73  	NewExecClient     func(namespace string) (exec.Executor, error)
    74  	RunListenerSocket func(*uniter.SocketConfig) (*sockets.Socket, error)
    75  
    76  	LoadOperatorInfo func(paths Paths) (*caas.OperatorInfo, error)
    77  
    78  	NewContainerStartWatcherClient func(Client) ContainerStartWatcher
    79  }
    80  
    81  func (config ManifoldConfig) Validate() error {
    82  	if config.Logger == nil {
    83  		return errors.NotValidf("missing Logger")
    84  	}
    85  	if config.AgentName == "" {
    86  		return errors.NotValidf("empty AgentName")
    87  	}
    88  	if config.APICallerName == "" {
    89  		return errors.NotValidf("empty APICallerName")
    90  	}
    91  	if config.ClockName == "" {
    92  		return errors.NotValidf("empty ClockName")
    93  	}
    94  	if config.NewWorker == nil {
    95  		return errors.NotValidf("missing NewWorker")
    96  	}
    97  	if config.NewClient == nil {
    98  		return errors.NotValidf("missing NewClient")
    99  	}
   100  	if config.NewCharmDownloader == nil {
   101  		return errors.NotValidf("missing NewCharmDownloader")
   102  	}
   103  	if config.CharmDirName == "" {
   104  		return errors.NotValidf("missing CharmDirName")
   105  	}
   106  	if config.ProfileDir == "" {
   107  		return errors.NotValidf("missing ProfileDir")
   108  	}
   109  	if config.MachineLock == nil {
   110  		return errors.NotValidf("missing MachineLock")
   111  	}
   112  	if config.HookRetryStrategyName == "" {
   113  		return errors.NotValidf("missing HookRetryStrategyName")
   114  	}
   115  	if config.LeadershipGuarantee == 0 {
   116  		return errors.NotValidf("missing LeadershipGuarantee")
   117  	}
   118  	if config.NewExecClient == nil {
   119  		return errors.NotValidf("missing NewExecClient")
   120  	}
   121  	return nil
   122  }
   123  
   124  // Manifold returns a dependency manifold that runs a caasoperator worker,
   125  // using the resource names defined in the supplied config.
   126  func Manifold(config ManifoldConfig) dependency.Manifold {
   127  	return dependency.Manifold{
   128  		Inputs: []string{
   129  			config.AgentName,
   130  			config.APICallerName,
   131  			config.ClockName,
   132  			config.CharmDirName,
   133  			config.HookRetryStrategyName,
   134  		},
   135  		Start: func(context dependency.Context) (worker.Worker, error) {
   136  			if err := config.Validate(); err != nil {
   137  				return nil, errors.Trace(err)
   138  			}
   139  
   140  			var agent agent.Agent
   141  			if err := context.Get(config.AgentName, &agent); err != nil {
   142  				return nil, errors.Trace(err)
   143  			}
   144  
   145  			var apiCaller base.APICaller
   146  			if err := context.Get(config.APICallerName, &apiCaller); err != nil {
   147  				return nil, errors.Trace(err)
   148  			}
   149  			client := config.NewClient(apiCaller)
   150  			downloader := config.NewCharmDownloader(apiCaller)
   151  
   152  			var clock clock.Clock
   153  			if err := context.Get(config.ClockName, &clock); err != nil {
   154  				return nil, errors.Trace(err)
   155  			}
   156  
   157  			model, err := client.Model()
   158  			if err != nil {
   159  				return nil, errors.Trace(err)
   160  			}
   161  
   162  			var charmDirGuard fortress.Guard
   163  			if err := context.Get(config.CharmDirName, &charmDirGuard); err != nil {
   164  				return nil, err
   165  			}
   166  
   167  			var hookRetryStrategy params.RetryStrategy
   168  			if err := context.Get(config.HookRetryStrategyName, &hookRetryStrategy); err != nil {
   169  				return nil, err
   170  			}
   171  
   172  			// Configure and start the caasoperator worker.
   173  			agentConfig := agent.CurrentConfig()
   174  			tag := agentConfig.Tag()
   175  			applicationTag, ok := tag.(names.ApplicationTag)
   176  			if !ok {
   177  				return nil, errors.Errorf("expected an application tag, got %v", tag)
   178  			}
   179  			newUniterFunc := func(unitTag names.UnitTag) *apiuniter.State {
   180  				return apiuniter.NewState(apiCaller, unitTag)
   181  			}
   182  			newResourcesFacadeFunc := func(unitTag names.UnitTag) (*apiuniter.ResourcesFacadeClient, error) {
   183  				return apiuniter.NewResourcesFacadeClient(apiCaller, unitTag)
   184  			}
   185  			newPayloadFacadeFunc := func() *apiuniter.PayloadFacadeClient {
   186  				return apiuniter.NewPayloadFacadeClient(apiCaller)
   187  			}
   188  			leadershipTrackerFunc := func(unitTag names.UnitTag) coreleadership.TrackerWorker {
   189  				claimer := apileadership.NewClient(apiCaller)
   190  				return leadership.NewTracker(unitTag, claimer, clock, config.LeadershipGuarantee)
   191  			}
   192  
   193  			runListenerSocketFunc := config.RunListenerSocket
   194  			if runListenerSocketFunc == nil {
   195  				runListenerSocketFunc = runListenerSocket
   196  			}
   197  			containerStartWatcherClient := config.NewContainerStartWatcherClient
   198  			if containerStartWatcherClient == nil {
   199  				containerStartWatcherClient = func(c Client) ContainerStartWatcher {
   200  					return c
   201  				}
   202  			}
   203  			jujuSecretsAPI := secretsmanager.NewClient(apiCaller)
   204  			secretsBackendGetter := func() (secrets.BackendsClient, error) {
   205  				return secrets.NewClient(jujuSecretsAPI)
   206  			}
   207  			secretRotateWatcherFunc := func(unitTag names.UnitTag, isLeader bool, rotateSecrets chan []string) (worker.Worker, error) {
   208  				owners := []names.Tag{unitTag}
   209  				if isLeader {
   210  					appName, _ := names.UnitApplication(unitTag.Id())
   211  					owners = append(owners, names.NewApplicationTag(appName))
   212  				}
   213  				return secretrotate.New(secretrotate.Config{
   214  					SecretManagerFacade: jujuSecretsAPI,
   215  					Clock:               clock,
   216  					Logger:              config.Logger.Child("secretsrotate"),
   217  					SecretOwners:        owners,
   218  					RotateSecrets:       rotateSecrets,
   219  				})
   220  			}
   221  			secretExpiryWatcherFunc := func(unitTag names.UnitTag, isLeader bool, expireRevisions chan []string) (worker.Worker, error) {
   222  				owners := []names.Tag{unitTag}
   223  				if isLeader {
   224  					appName, _ := names.UnitApplication(unitTag.Id())
   225  					owners = append(owners, names.NewApplicationTag(appName))
   226  				}
   227  				return secretexpire.New(secretexpire.Config{
   228  					SecretManagerFacade: jujuSecretsAPI,
   229  					Clock:               clock,
   230  					Logger:              config.Logger.Child("secretsexpire"),
   231  					SecretOwners:        owners,
   232  					ExpireRevisions:     expireRevisions,
   233  				})
   234  			}
   235  
   236  			wCfg := Config{
   237  				Logger:                config.Logger,
   238  				ModelUUID:             agentConfig.Model().Id(),
   239  				ModelName:             model.Name,
   240  				Application:           applicationTag.Id(),
   241  				CharmGetter:           client,
   242  				Clock:                 clock,
   243  				DataDir:               agentConfig.DataDir(),
   244  				ProfileDir:            config.ProfileDir,
   245  				Downloader:            downloader,
   246  				StatusSetter:          client,
   247  				UnitGetter:            client,
   248  				UnitRemover:           client,
   249  				ApplicationWatcher:    client,
   250  				ContainerStartWatcher: containerStartWatcherClient(client),
   251  				VersionSetter:         client,
   252  				StartUniterFunc:       uniter.StartUniter,
   253  				RunListenerSocketFunc: runListenerSocketFunc,
   254  				LeadershipTrackerFunc: leadershipTrackerFunc,
   255  				UniterFacadeFunc:      newUniterFunc,
   256  				ResourcesFacadeFunc:   newResourcesFacadeFunc,
   257  				PayloadFacadeFunc:     newPayloadFacadeFunc,
   258  				ExecClientGetter: func() (exec.Executor, error) {
   259  					return config.NewExecClient(os.Getenv(caasconstants.OperatorNamespaceEnvName))
   260  				},
   261  			}
   262  
   263  			loadOperatorInfoFunc := config.LoadOperatorInfo
   264  			if loadOperatorInfoFunc == nil {
   265  				loadOperatorInfoFunc = LoadOperatorInfo
   266  			}
   267  			operatorInfo, err := loadOperatorInfoFunc(wCfg.getPaths())
   268  			if err != nil {
   269  				return nil, errors.Trace(err)
   270  			}
   271  			wCfg.OperatorInfo = *operatorInfo
   272  			wCfg.UniterParams = &uniter.UniterParams{
   273  				NewOperationExecutor:    operation.NewExecutor,
   274  				NewDeployer:             charm.NewDeployer,
   275  				NewProcessRunner:        runner.NewRunner,
   276  				DataDir:                 agentConfig.DataDir(),
   277  				Clock:                   clock,
   278  				MachineLock:             config.MachineLock,
   279  				CharmDirGuard:           charmDirGuard,
   280  				UpdateStatusSignal:      uniter.NewUpdateStatusTimer(),
   281  				HookRetryStrategy:       hookRetryStrategy,
   282  				TranslateResolverErr:    config.TranslateResolverErr,
   283  				SecretsClient:           jujuSecretsAPI,
   284  				SecretsBackendGetter:    secretsBackendGetter,
   285  				SecretRotateWatcherFunc: secretRotateWatcherFunc,
   286  				SecretExpiryWatcherFunc: secretExpiryWatcherFunc,
   287  				Logger:                  wCfg.Logger.Child("uniter"),
   288  			}
   289  			wCfg.UniterParams.SocketConfig, err = socketConfig(operatorInfo)
   290  			if err != nil {
   291  				return nil, errors.Trace(err)
   292  			}
   293  
   294  			w, err := config.NewWorker(wCfg)
   295  			if err != nil {
   296  				return nil, errors.Trace(err)
   297  			}
   298  			return w, nil
   299  		},
   300  	}
   301  }
   302  
   303  func socketConfig(info *caas.OperatorInfo) (*uniter.SocketConfig, error) {
   304  	tlsCert, err := tls.X509KeyPair([]byte(info.Cert), []byte(info.PrivateKey))
   305  	if err != nil {
   306  		return nil, errors.Annotatef(err, "cannot parse operator TLS certificate")
   307  	}
   308  
   309  	block, _ := pem.Decode([]byte(info.CACert))
   310  	tlsCert.Certificate = append(tlsCert.Certificate, block.Bytes)
   311  	tlsConfig := http.SecureTLSConfig()
   312  	tlsConfig.Certificates = []tls.Certificate{tlsCert}
   313  
   314  	serviceAddress := os.Getenv(caasconstants.OperatorServiceIPEnvName)
   315  	if serviceAddress == "" {
   316  		return nil, errors.Errorf("env %s missing", caasconstants.OperatorServiceIPEnvName)
   317  	}
   318  
   319  	operatorAddress := os.Getenv(caasconstants.OperatorPodIPEnvName)
   320  	if operatorAddress == "" {
   321  		return nil, errors.Errorf("env %s missing", caasconstants.OperatorPodIPEnvName)
   322  	}
   323  
   324  	sc := &uniter.SocketConfig{
   325  		ServiceAddress:  serviceAddress,
   326  		OperatorAddress: operatorAddress,
   327  		TLSConfig:       tlsConfig,
   328  	}
   329  	return sc, nil
   330  }
   331  
   332  // LoadOperatorInfo loads the operator info file from the state dir.
   333  func LoadOperatorInfo(paths Paths) (*caas.OperatorInfo, error) {
   334  	filepath := path.Join(paths.State.BaseDir, caas.OperatorInfoFile)
   335  	data, err := os.ReadFile(filepath)
   336  	if err != nil {
   337  		return nil, errors.Annotatef(err, "reading operator info file %s", filepath)
   338  	}
   339  	return caas.UnmarshalOperatorInfo(data)
   340  }