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 }