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

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package secretsdrainworker
     5  
     6  import (
     7  	"context"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/worker/v3"
    11  	"github.com/juju/worker/v3/catacomb"
    12  
    13  	"github.com/juju/juju/api/common/secretsdrain"
    14  	coresecrets "github.com/juju/juju/core/secrets"
    15  	"github.com/juju/juju/core/watcher"
    16  	jujusecrets "github.com/juju/juju/secrets"
    17  )
    18  
    19  // logger is here to stop the desire of creating a package level logger.
    20  // Don't do this, instead use the one passed as manifold config.
    21  type logger interface{}
    22  
    23  var _ logger = struct{}{}
    24  
    25  // Logger represents the methods used by the worker to log information.
    26  type Logger interface {
    27  	Debugf(string, ...interface{})
    28  	Warningf(string, ...interface{})
    29  	Infof(string, ...interface{})
    30  }
    31  
    32  // SecretsDrainFacade instances provide a set of API for the worker to deal with secret drain process.
    33  type SecretsDrainFacade interface {
    34  	WatchSecretBackendChanged() (watcher.NotifyWatcher, error)
    35  	GetSecretsToDrain() ([]coresecrets.SecretMetadataForDrain, error)
    36  	ChangeSecretBackend([]secretsdrain.ChangeSecretBackendArg) (secretsdrain.ChangeSecretBackendResult, error)
    37  }
    38  
    39  // Config defines the operation of the Worker.
    40  type Config struct {
    41  	SecretsDrainFacade
    42  	Logger Logger
    43  
    44  	SecretsBackendGetter func() (jujusecrets.BackendsClient, error)
    45  }
    46  
    47  // Validate returns an error if config cannot drive the Worker.
    48  func (config Config) Validate() error {
    49  	if config.SecretsDrainFacade == nil {
    50  		return errors.NotValidf("nil SecretsDrainFacade")
    51  	}
    52  	if config.Logger == nil {
    53  		return errors.NotValidf("nil Logger")
    54  	}
    55  	if config.SecretsBackendGetter == nil {
    56  		return errors.NotValidf("nil SecretsBackendGetter")
    57  	}
    58  	return nil
    59  }
    60  
    61  // NewWorker returns a secretsdrainworker Worker backed by config, or an error.
    62  func NewWorker(config Config) (worker.Worker, error) {
    63  	if err := config.Validate(); err != nil {
    64  		return nil, errors.Trace(err)
    65  	}
    66  
    67  	w := &Worker{config: config}
    68  	err := catacomb.Invoke(catacomb.Plan{
    69  		Site: &w.catacomb,
    70  		Work: w.loop,
    71  	})
    72  	return w, errors.Trace(err)
    73  }
    74  
    75  // Worker drains secrets to the new backend when the model's secret backend has changed.
    76  type Worker struct {
    77  	catacomb catacomb.Catacomb
    78  	config   Config
    79  }
    80  
    81  // TODO(secrets): user created secrets should be drained on the controller because they do not have an owner unit!
    82  
    83  // Kill is defined on worker.Worker.
    84  func (w *Worker) Kill() {
    85  	w.catacomb.Kill(nil)
    86  }
    87  
    88  // Wait is part of the worker.Worker interface.
    89  func (w *Worker) Wait() error {
    90  	return w.catacomb.Wait()
    91  }
    92  
    93  func (w *Worker) loop() (err error) {
    94  	watcher, err := w.config.SecretsDrainFacade.WatchSecretBackendChanged()
    95  	if err != nil {
    96  		return errors.Trace(err)
    97  	}
    98  	if err := w.catacomb.Add(watcher); err != nil {
    99  		return errors.Trace(err)
   100  	}
   101  
   102  	for {
   103  		select {
   104  		case <-w.catacomb.Dying():
   105  			return errors.Trace(w.catacomb.ErrDying())
   106  		case _, ok := <-watcher.Changes():
   107  			if !ok {
   108  				return errors.New("secret backend changed watch closed")
   109  			}
   110  			w.config.Logger.Debugf("got new secret backend")
   111  
   112  			secrets, err := w.config.SecretsDrainFacade.GetSecretsToDrain()
   113  			if err != nil {
   114  				return errors.Trace(err)
   115  			}
   116  			if len(secrets) == 0 {
   117  				w.config.Logger.Debugf("no secrets to drain")
   118  				continue
   119  			}
   120  			w.config.Logger.Debugf("got %d secrets to drain", len(secrets))
   121  			backends, err := w.config.SecretsBackendGetter()
   122  			if err != nil {
   123  				return errors.Trace(err)
   124  			}
   125  			for _, md := range secrets {
   126  				if err := w.drainSecret(md, backends); err != nil {
   127  					return errors.Trace(err)
   128  				}
   129  			}
   130  		}
   131  	}
   132  }
   133  
   134  func (w *Worker) drainSecret(md coresecrets.SecretMetadataForDrain, client jujusecrets.BackendsClient) (err error) {
   135  	var args []secretsdrain.ChangeSecretBackendArg
   136  	var cleanUpInExternalBackendFuncs []func() error
   137  	for _, revisionMeta := range md.Revisions {
   138  		rev := revisionMeta
   139  		// We have to get the active backend for each drain operation because the active backend
   140  		// could be changed during the draining process.
   141  		activeBackend, activeBackendID, err := client.GetBackend(nil, true)
   142  		if err != nil {
   143  			return errors.Trace(err)
   144  		}
   145  		if rev.ValueRef != nil && rev.ValueRef.BackendID == activeBackendID {
   146  			w.config.Logger.Debugf("secret %q revision %d is already on the active backend %q", md.Metadata.URI, rev.Revision, activeBackendID)
   147  			continue
   148  		}
   149  		w.config.Logger.Debugf("draining %s/%d", md.Metadata.URI.ID, rev.Revision)
   150  
   151  		secretVal, err := client.GetRevisionContent(md.Metadata.URI, rev.Revision)
   152  		if err != nil {
   153  			return errors.Trace(err)
   154  		}
   155  		newRevId, err := activeBackend.SaveContent(context.TODO(), md.Metadata.URI, rev.Revision, secretVal)
   156  		if err != nil && !errors.Is(err, errors.NotSupported) {
   157  			return errors.Trace(err)
   158  		}
   159  		w.config.Logger.Debugf("saved secret %s/%d to the new backend %q, %#v", md.Metadata.URI.ID, rev.Revision, activeBackendID, err)
   160  		var newValueRef *coresecrets.ValueRef
   161  		data := secretVal.EncodedValues()
   162  		if err == nil {
   163  			// We are draining to an external backend,
   164  			newValueRef = &coresecrets.ValueRef{
   165  				BackendID:  activeBackendID,
   166  				RevisionID: newRevId,
   167  			}
   168  			// The content has successfully saved into the external backend.
   169  			// So we won't save the content into the Juju database.
   170  			data = nil
   171  		}
   172  
   173  		cleanUpInExternalBackend := func() error { return nil }
   174  		if rev.ValueRef != nil {
   175  			// The old backend is an external backend.
   176  			// Note: we have to get the old backend before we make ChangeSecretBackend facade call.
   177  			// Because the token policy(for the vault backend especially) will be changed after we changed the secret's backend.
   178  			oldBackend, _, err := client.GetBackend(&rev.ValueRef.BackendID, true)
   179  			if err != nil {
   180  				return errors.Trace(err)
   181  			}
   182  			cleanUpInExternalBackend = func() error {
   183  				w.config.Logger.Debugf("cleanup secret %s/%d from old backend %q", md.Metadata.URI.ID, rev.Revision, rev.ValueRef.BackendID)
   184  				if activeBackendID == rev.ValueRef.BackendID {
   185  					// Ideally, We should have done all these drain steps in the controller via transaction, but by design, we only allow
   186  					// uniters to be able to access secret content. So we have to do these extra checks to avoid
   187  					// secret gets deleted wrongly when the model's secret backend is changed back to
   188  					// the old backend while the secret is being drained.
   189  					return nil
   190  				}
   191  				err := oldBackend.DeleteContent(context.TODO(), rev.ValueRef.RevisionID)
   192  				if errors.Is(err, errors.NotFound) {
   193  					// This should never happen, but if it does, we can just ignore.
   194  					return nil
   195  				}
   196  				return errors.Trace(err)
   197  			}
   198  		}
   199  		cleanUpInExternalBackendFuncs = append(cleanUpInExternalBackendFuncs, cleanUpInExternalBackend)
   200  		args = append(args, secretsdrain.ChangeSecretBackendArg{
   201  			URI:      md.Metadata.URI,
   202  			Revision: rev.Revision,
   203  			ValueRef: newValueRef,
   204  			Data:     data,
   205  		})
   206  	}
   207  	if len(args) == 0 {
   208  		return nil
   209  	}
   210  
   211  	w.config.Logger.Debugf("content moved, updating backend info")
   212  	results, err := w.config.SecretsDrainFacade.ChangeSecretBackend(args)
   213  	if err != nil {
   214  		return errors.Trace(err)
   215  	}
   216  
   217  	for i, err := range results.Results {
   218  		arg := args[i]
   219  		if err == nil {
   220  			// We have already changed the secret to the active backend, so we
   221  			// can clean up the secret content in the old backend now.
   222  			if err := cleanUpInExternalBackendFuncs[i](); err != nil {
   223  				w.config.Logger.Warningf("failed to clean up secret %q-%d in the external backend: %v", arg.URI, arg.Revision, err)
   224  			}
   225  		} else {
   226  			// If any of the ChangeSecretBackend calls failed, we will
   227  			// bounce the agent to retry those failed tasks.
   228  			w.config.Logger.Warningf("failed to change secret backend for %q-%d: %v", arg.URI, arg.Revision, err)
   229  		}
   230  	}
   231  	if results.ErrorCount() > 0 {
   232  		// We got failed tasks, so we have to bounce the agent to retry those failed tasks.
   233  		return errors.Errorf("failed to drain secret revisions for %q to the active backend", md.Metadata.URI)
   234  	}
   235  	return nil
   236  }