github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/unit_ops.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "github.com/juju/errors" 8 "github.com/juju/mgo/v3" 9 "github.com/juju/mgo/v3/bson" 10 "github.com/juju/mgo/v3/txn" 11 jujutxn "github.com/juju/txn/v3" 12 13 "github.com/juju/juju/core/quota" 14 mgoutils "github.com/juju/juju/mongo/utils" 15 ) 16 17 type unitSetStateOperation struct { 18 u *Unit 19 newState *UnitState 20 21 // Quota limits for updating the charm and uniter state data. 22 limits UnitStateSizeLimits 23 } 24 25 // Build implements ModelOperation. 26 func (op *unitSetStateOperation) Build(attempt int) ([]txn.Op, error) { 27 if op.newState == nil || !op.newState.Modified() { 28 return nil, jujutxn.ErrNoOperations 29 } 30 return op.buildTxn(attempt) 31 } 32 33 func (op *unitSetStateOperation) buildTxn(attempt int) ([]txn.Op, error) { 34 if attempt > 0 { 35 if err := op.u.Refresh(); err != nil { 36 return nil, errors.Annotatef(err, "cannot persist state for unit %q", op.u) 37 } 38 } 39 40 // Normally this would be if Life() != Alive. However the uniter 41 // needs to write its state during the Dying period to complete 42 // operations such as resigning leadership. 43 if op.u.Life() == Dead { 44 return nil, errors.Annotatef(errors.NotFoundf("unit %s", op.u.Name()), "cannot persist state for unit %q", op.u) 45 } 46 47 coll, closer := op.u.st.db().GetCollection(unitStatesC) 48 defer closer() 49 50 // The state of a unit can only be updated if it is currently alive. 51 unitNotDeadOp := txn.Op{ 52 C: unitsC, 53 Id: op.u.doc.DocID, 54 Assert: notDeadDoc, 55 } 56 57 var stDoc unitStateDoc 58 unitGlobalKey := op.u.globalKey() 59 if err := coll.FindId(unitGlobalKey).One(&stDoc); err != nil { 60 if err != mgo.ErrNotFound { 61 return nil, errors.Annotatef(err, "cannot persist state for unit %q", op.u) 62 } 63 64 // Create new doc and enforce quota limits 65 newDoc, err := op.newUnitStateDoc(unitGlobalKey) 66 if err != nil { 67 return nil, errors.Trace(err) 68 } 69 return []txn.Op{unitNotDeadOp, { 70 C: unitStatesC, 71 Id: unitGlobalKey, 72 Assert: txn.DocMissing, 73 Insert: newDoc, 74 }}, nil 75 } 76 77 // We have an existing doc, see what changes need to be made. 78 setFields, unsetFields, err := op.fields(stDoc) 79 if err != nil { 80 return nil, errors.Trace(err) 81 } else if len(setFields) <= 0 && len(unsetFields) <= 0 { 82 return nil, jujutxn.ErrNoOperations 83 } 84 85 updateFields := bson.D{} 86 if len(setFields) > 0 { 87 updateFields = append(updateFields, bson.DocElem{"$set", setFields}) 88 } 89 if len(unsetFields) > 0 { 90 updateFields = append(updateFields, bson.DocElem{"$unset", unsetFields}) 91 } 92 return []txn.Op{unitNotDeadOp, { 93 C: unitStatesC, 94 Id: unitGlobalKey, 95 Assert: bson.D{ 96 {"txn-revno", stDoc.TxnRevno}, 97 }, 98 Update: updateFields, 99 }}, nil 100 } 101 102 func (op *unitSetStateOperation) newUnitStateDoc(unitGlobalKey string) (unitStateDoc, error) { 103 newStDoc := unitStateDoc{ 104 DocID: unitGlobalKey, 105 } 106 if chState, found := op.newState.CharmState(); found { 107 escapedCharmState := make(map[string]string, len(chState)) 108 for k, v := range chState { 109 escapedCharmState[mgoutils.EscapeKey(k)] = v 110 } 111 newStDoc.CharmState = escapedCharmState 112 113 quotaChecker := op.getCharmStateQuotaChecker() 114 quotaChecker.Check(newStDoc.CharmState) 115 if err := quotaChecker.Outcome(); err != nil { 116 return unitStateDoc{}, errors.Annotatef(err, "persisting charm state") 117 } 118 } 119 120 quotaChecker := op.getUniterStateQuotaChecker() 121 if rState, found := op.newState.relationStateBSONFriendly(); found { 122 newStDoc.RelationState = rState 123 quotaChecker.Check(rState) 124 } 125 if uniterState, found := op.newState.UniterState(); found { 126 newStDoc.UniterState = uniterState 127 quotaChecker.Check(uniterState) 128 } 129 if storState, found := op.newState.StorageState(); found { 130 newStDoc.StorageState = storState 131 quotaChecker.Check(storState) 132 } 133 if secretState, found := op.newState.SecretState(); found { 134 newStDoc.SecretState = secretState 135 quotaChecker.Check(secretState) 136 } 137 if meterStatusState, found := op.newState.MeterStatusState(); found { 138 newStDoc.MeterStatusState = meterStatusState 139 quotaChecker.Check(meterStatusState) 140 } 141 if err := quotaChecker.Outcome(); err != nil { 142 return unitStateDoc{}, errors.Annotatef(err, "persisting uniter state") 143 } 144 return newStDoc, nil 145 } 146 147 // fields returns set and unset bson required to update the unit state doc 148 // based the current data stored compared to this operation. 149 func (op *unitSetStateOperation) fields(currentDoc unitStateDoc) (bson.D, bson.D, error) { 150 // Handling fields of op.newState: 151 // If a pointer is nil, ignore it. 152 // If the value referenced by the pointer is empty, remove that thing. 153 // If there is a value referenced by the pointer, set the value if a string, or merge the data. 154 setFields := bson.D{} 155 unsetFields := bson.D{} 156 157 // Check if we need to update the charm state 158 if chState, found := op.newState.CharmState(); found { 159 if len(chState) == 0 { 160 unsetFields = append(unsetFields, bson.DocElem{Name: "charm-state"}) 161 } else { 162 // State keys may contain dots or dollar chars which need to be escaped. 163 escapedCharmState := make(bson.M, len(chState)) 164 for k, v := range chState { 165 escapedCharmState[mgoutils.EscapeKey(k)] = v 166 } 167 if !currentDoc.charmStateMatches(escapedCharmState) { 168 setFields = append(setFields, bson.DocElem{"charm-state", escapedCharmState}) 169 170 quotaChecker := op.getCharmStateQuotaChecker() 171 quotaChecker.Check(escapedCharmState) 172 if err := quotaChecker.Outcome(); err != nil { 173 if errors.IsQuotaLimitExceeded(err) { 174 return nil, nil, errors.Annotatef(err, "persisting charm state") 175 } 176 return nil, nil, errors.Trace(err) 177 } 178 } 179 } 180 } 181 182 // Enforce max uniter internal state size by accumulating the size of 183 // the various uniter-related state bits. 184 quotaChecker := op.getUniterStateQuotaChecker() 185 if uniterState, found := op.newState.UniterState(); found { 186 if uniterState == "" { 187 unsetFields = append(unsetFields, bson.DocElem{Name: "uniter-state"}) 188 } else if uniterState != currentDoc.UniterState { 189 setFields = append(setFields, bson.DocElem{"uniter-state", uniterState}) 190 quotaChecker.Check(uniterState) 191 } 192 } else { 193 quotaChecker.Check(currentDoc.UniterState) 194 } 195 196 if rState, found := op.newState.relationStateBSONFriendly(); found { 197 if len(rState) == 0 { 198 unsetFields = append(unsetFields, bson.DocElem{Name: "relation-state"}) 199 } else if matches := currentDoc.relationStateMatches(rState); !matches { 200 setFields = append(setFields, bson.DocElem{"relation-state", rState}) 201 quotaChecker.Check(rState) 202 } 203 } else { 204 quotaChecker.Check(currentDoc.RelationState) 205 } 206 207 if storState, found := op.newState.StorageState(); found { 208 if storState == "" { 209 unsetFields = append(unsetFields, bson.DocElem{Name: "storage-state"}) 210 } else if storState != currentDoc.StorageState { 211 setFields = append(setFields, bson.DocElem{"storage-state", storState}) 212 quotaChecker.Check(storState) 213 } 214 } 215 216 if secretState, found := op.newState.SecretState(); found { 217 if secretState == "" { 218 unsetFields = append(unsetFields, bson.DocElem{Name: "secret-state"}) 219 } else if secretState != currentDoc.SecretState { 220 setFields = append(setFields, bson.DocElem{"secret-state", secretState}) 221 quotaChecker.Check(secretState) 222 } 223 } 224 225 if meterStatusState, found := op.newState.MeterStatusState(); found { 226 if meterStatusState == "" { 227 unsetFields = append(unsetFields, bson.DocElem{Name: "meter-status-state"}) 228 } else if meterStatusState != currentDoc.MeterStatusState { 229 setFields = append(setFields, bson.DocElem{"meter-status-state", meterStatusState}) 230 quotaChecker.Check(meterStatusState) 231 } 232 } 233 234 if err := quotaChecker.Outcome(); err != nil { 235 if errors.IsQuotaLimitExceeded(err) { 236 return nil, nil, errors.Annotatef(err, "persisting internal uniter state") 237 } 238 return nil, nil, errors.Trace(err) 239 } 240 241 return setFields, unsetFields, nil 242 } 243 244 func (op *unitSetStateOperation) getCharmStateQuotaChecker() quota.Checker { 245 // Enforce max key/value length (fixed) and maximum 246 // charm state size (configured by the operator). 247 return quota.NewMultiChecker( 248 quota.NewMapKeyValueSizeChecker(quota.MaxCharmStateKeySize, quota.MaxCharmStateValueSize), 249 quota.NewBSONTotalSizeChecker(op.limits.MaxCharmStateSize), 250 ) 251 } 252 253 func (op *unitSetStateOperation) getUniterStateQuotaChecker() quota.Checker { 254 return quota.NewMultiChecker( 255 quota.NewBSONTotalSizeChecker(op.limits.MaxAgentStateSize), 256 ) 257 } 258 259 // Done implements ModelOperation. 260 func (op *unitSetStateOperation) Done(err error) error { return err }