github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/modelcredential.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/errors" 11 "github.com/juju/mgo/v3/bson" 12 "github.com/juju/mgo/v3/txn" 13 "github.com/juju/names/v5" 14 jujutxn "github.com/juju/txn/v3" 15 16 "github.com/juju/juju/cloud" 17 "github.com/juju/juju/core/status" 18 ) 19 20 // InvalidateModelCredential invalidate cloud credential for the model 21 // of the given state. 22 func (st *State) InvalidateModelCredential(reason string) error { 23 m, err := st.Model() 24 if err != nil { 25 return errors.Trace(err) 26 } 27 28 tag, exists := m.CloudCredentialTag() 29 if !exists { 30 // Model is on the cloud that does not require auth - nothing to do. 31 return nil 32 } 33 34 if err := st.InvalidateCloudCredential(tag, reason); err != nil { 35 return errors.Trace(err) 36 } 37 if err := st.suspendCredentialModels(tag, reason); err != nil { 38 // These updates are optimistic. If they fail, it's unfortunate but we are not going to stop the call. 39 logger.Warningf("could not suspend models that use credential %v: %v", tag.Id(), err) 40 } 41 return nil 42 } 43 44 func (st *State) suspendCredentialModels(tag names.CloudCredentialTag, reason string) error { 45 models, err := st.modelsWithCredential(tag) 46 if err != nil { 47 return errors.Annotatef(err, "could not determine what models use credential %v", tag.Id()) 48 } 49 infos := make([]string, len(models)) 50 for i, m := range models { 51 infos[i] = fmt.Sprintf("%s (%s)", m.Name, m.UUID) 52 } 53 logger.Warningf("suspending these models:\n%s\n because their credential has become invalid:\n%s", 54 strings.Join(infos, " - "), 55 reason) 56 sts := modelStatusInvalidCredential(reason) 57 doc := statusDoc{ 58 Status: sts.Status, 59 StatusInfo: sts.Message, 60 StatusData: sts.Data, 61 Updated: timeOrNow(nil, st.clock()).UnixNano(), 62 } 63 for _, m := range models { 64 st.maybeSetModelStatusHistoryDoc(m.UUID, doc) 65 } 66 return nil 67 } 68 69 func (st *State) maybeSetModelStatusHistoryDoc(modelUUID string, doc statusDoc) { 70 one, closer, err := st.model(modelUUID) 71 defer func() { _ = closer() }() 72 if err != nil { 73 logger.Warningf("model %v error: %v", modelUUID, err) 74 return 75 } 76 77 if _, err = probablyUpdateStatusHistory(one.st.db(), one.globalKey(), doc); err != nil { 78 logger.Warningf("%v", err) 79 } 80 } 81 82 // ValidateCloudCredential validates new cloud credential for this model. 83 func (m *Model) ValidateCloudCredential(tag names.CloudCredentialTag, credential cloud.Credential) error { 84 aCloud, err := m.st.Cloud(m.CloudName()) 85 if err != nil { 86 return errors.Annotatef(err, "getting cloud %q", m.CloudName()) 87 } 88 89 err = validateCredentialForCloud(aCloud, tag, convertCloudCredentialToState(tag, credential)) 90 if err != nil { 91 return errors.Annotatef(err, "validating credential %q for cloud %q", tag.Id(), aCloud.Name) 92 } 93 return nil 94 } 95 96 // SetCloudCredential sets new cloud credential for this model. 97 // Returned bool indicates if model credential was set. 98 func (m *Model) SetCloudCredential(tag names.CloudCredentialTag) (bool, error) { 99 // If model is suspended, after this call, it may be reverted since, 100 // if updated, model credential will be set to a valid credential. 101 modelStatus, err := m.Status() 102 if err != nil { 103 return false, errors.Annotatef(err, "getting model status %q", m.UUID()) 104 } 105 revert := modelStatus.Status == status.Suspended 106 aCloud, err := m.st.Cloud(m.CloudName()) 107 if err != nil { 108 return false, errors.Annotatef(err, "getting cloud %q", m.CloudName()) 109 } 110 updating := true 111 buildTxn := func(attempt int) ([]txn.Op, error) { 112 if attempt > 0 { 113 if err := m.Refresh(); err != nil { 114 return nil, errors.Trace(err) 115 } 116 } 117 if tag.Id() == m.doc.CloudCredential { 118 updating = false 119 return nil, jujutxn.ErrNoOperations 120 } 121 // Must be a valid credential that is already on the controller. 122 credential, err := m.st.CloudCredential(tag) 123 if err != nil { 124 return nil, errors.Trace(err) 125 } 126 if !credential.IsValid() { 127 return nil, errors.NotValidf("credential %q", tag.Id()) 128 } 129 if err := validateCredentialForCloud(aCloud, tag, credential); err != nil { 130 return nil, errors.Trace(err) 131 } 132 return []txn.Op{{ 133 C: modelsC, 134 Id: m.doc.UUID, 135 Assert: txn.DocExists, 136 Update: bson.D{{"$set", bson.D{{"cloud-credential", tag.Id()}}}}, 137 }}, nil 138 } 139 if err := m.st.db().Run(buildTxn); err != nil { 140 return false, errors.Trace(err) 141 } 142 if updating && revert { 143 if err := m.maybeRevertModelStatus(); err != nil { 144 logger.Warningf("could not revert status for model %v: %v", m.UUID(), err) 145 } 146 } 147 return updating, m.Refresh() 148 } 149 150 // WatchModelCredential returns a new NotifyWatcher that watches 151 // a model reference to a cloud credential. 152 func (m *Model) WatchModelCredential() NotifyWatcher { 153 current := m.doc.CloudCredential 154 modelUUID := m.doc.UUID 155 filter := func(id interface{}) bool { 156 id, ok := id.(string) 157 if !ok || id != modelUUID { 158 return false 159 } 160 161 models, closer := m.st.db().GetCollection(modelsC) 162 defer closer() 163 164 var doc *modelDoc 165 if err := models.FindId(id).One(&doc); err != nil { 166 return false 167 } 168 169 match := current != doc.CloudCredential 170 current = doc.CloudCredential 171 return match 172 } 173 return newNotifyCollWatcher(m.st, modelsC, filter) 174 }