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 }