github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/podspec_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/charm/v12"
     8  	"github.com/juju/errors"
     9  	"github.com/juju/mgo/v3/bson"
    10  	"github.com/juju/mgo/v3/txn"
    11  	"github.com/juju/names/v5"
    12  
    13  	"github.com/juju/juju/core/leadership"
    14  )
    15  
    16  type setPodSpecOperation struct {
    17  	m       *CAASModel
    18  	appTag  names.ApplicationTag
    19  	spec    *string
    20  	rawSpec *string
    21  
    22  	tokenAwareTxnBuilder func(int) ([]txn.Op, error)
    23  }
    24  
    25  // newSetPodSpecOperation returns a ModelOperation for updating the PodSpec or
    26  // for a particular application. A nil token can be specified to bypass the
    27  // leadership check.
    28  func newSetPodSpecOperation(model *CAASModel, token leadership.Token, appTag names.ApplicationTag, spec *string) *setPodSpecOperation {
    29  	op := &setPodSpecOperation{
    30  		m:      model,
    31  		appTag: appTag,
    32  		spec:   spec,
    33  	}
    34  
    35  	if token != nil {
    36  		op.tokenAwareTxnBuilder = buildTxnWithLeadership(op.buildTxn, token)
    37  	}
    38  	return op
    39  }
    40  
    41  // newSetRawK8sSpecOperation returns a ModelOperation for updating the raw k8s spec
    42  // for a particular application. A nil token can be specified to bypass the
    43  // leadership check.
    44  func newSetRawK8sSpecOperation(model *CAASModel, token leadership.Token, appTag names.ApplicationTag, rawSpec *string) *setPodSpecOperation {
    45  	op := &setPodSpecOperation{
    46  		m:       model,
    47  		appTag:  appTag,
    48  		rawSpec: rawSpec,
    49  	}
    50  
    51  	if token != nil {
    52  		op.tokenAwareTxnBuilder = buildTxnWithLeadership(op.buildTxn, token)
    53  	}
    54  	return op
    55  }
    56  
    57  // Build implements ModelOperation.
    58  func (op *setPodSpecOperation) Build(attempt int) ([]txn.Op, error) {
    59  	if op.tokenAwareTxnBuilder != nil {
    60  		return op.tokenAwareTxnBuilder(attempt)
    61  	}
    62  	return op.buildTxn(attempt)
    63  }
    64  
    65  func (op *setPodSpecOperation) buildTxn(_ int) ([]txn.Op, error) {
    66  	if op.spec != nil && op.rawSpec != nil {
    67  		return nil, errors.NewForbidden(nil, "either spec or raw k8s spec can be set for each application, but not both")
    68  	}
    69  
    70  	var prereqOps []txn.Op
    71  	appName := op.appTag.Id()
    72  	app, err := op.m.State().Application(appName)
    73  	if err != nil {
    74  		return nil, errors.Annotate(err, "setting pod spec")
    75  	}
    76  	if app.Life() == Dead {
    77  		return nil, errors.NotValidf("setting pod-spec on dead application %s", appName)
    78  	}
    79  	// The app's charm may not be there yet (as is the case when migrating).
    80  	// This check is for checking the k8s-spec-set/k8s-raw-set call.
    81  	ch, _, err := app.Charm()
    82  	if err != nil && !errors.IsNotFound(err) {
    83  		return nil, errors.Trace(err)
    84  	} else if err == nil && (op.spec != nil || op.rawSpec != nil) {
    85  		if ch.Meta().Deployment != nil && ch.Meta().Deployment.DeploymentMode == charm.ModeOperator {
    86  			return nil, errors.New("cannot set k8s spec on an operator charm")
    87  		}
    88  		if charm.MetaFormat(ch) >= charm.FormatV2 {
    89  			return nil, errors.New("cannot set k8s spec on a v2 charm")
    90  		}
    91  	}
    92  	prereqOps = append(prereqOps, txn.Op{
    93  		C:      applicationsC,
    94  		Id:     app.doc.DocID,
    95  		Assert: notDeadDoc,
    96  	})
    97  
    98  	sop := txn.Op{
    99  		C:  podSpecsC,
   100  		Id: applicationGlobalKey(appName),
   101  	}
   102  	existing, err := op.m.podInfo(op.appTag)
   103  	if err == nil {
   104  		asserts := bson.D{{Name: "upgrade-counter", Value: existing.UpgradeCounter}}
   105  		if existing.UpgradeCounter == 0 {
   106  			asserts = bson.D{
   107  				bson.DocElem{
   108  					Name: "$or", Value: []bson.D{
   109  						{{Name: "upgrade-counter", Value: 0}},
   110  						{{
   111  							Name: "upgrade-counter",
   112  							Value: bson.D{
   113  								{Name: "$exists", Value: false},
   114  							},
   115  						}},
   116  					},
   117  				},
   118  			}
   119  		}
   120  		updates := bson.D{{Name: "$inc", Value: bson.D{{"upgrade-counter", 1}}}}
   121  		// Either "spec" or "raw-spec" can be set for each application.
   122  		if op.spec != nil {
   123  			updates = append(updates, bson.DocElem{Name: "$set", Value: bson.D{{"spec", *op.spec}}})
   124  			asserts = append(asserts, getEmptyStringFieldAssert("raw-spec"))
   125  		}
   126  		if op.rawSpec != nil {
   127  			updates = append(updates, bson.DocElem{Name: "$set", Value: bson.D{{"raw-spec", *op.rawSpec}}})
   128  			asserts = append(asserts, getEmptyStringFieldAssert("spec"))
   129  		}
   130  		sop.Assert = asserts
   131  		sop.Update = updates
   132  	} else if errors.IsNotFound(err) {
   133  		sop.Assert = txn.DocMissing
   134  		newDoc := containerSpecDoc{}
   135  		if op.spec != nil {
   136  			newDoc.Spec = *op.spec
   137  		}
   138  		if op.rawSpec != nil {
   139  			newDoc.RawSpec = *op.rawSpec
   140  		}
   141  		sop.Insert = newDoc
   142  	} else {
   143  		return nil, errors.Annotate(err, "setting pod spec")
   144  	}
   145  	return append(prereqOps, sop), nil
   146  }
   147  
   148  // Done implements ModelOperation.
   149  func (op *setPodSpecOperation) Done(err error) error { return err }
   150  
   151  func getEmptyStringFieldAssert(fieldName string) bson.DocElem {
   152  	return bson.DocElem{
   153  		Name: "$or", Value: []bson.D{
   154  			{{Name: fieldName, Value: ""}},
   155  			{{
   156  				Name: fieldName,
   157  				Value: bson.D{
   158  					{Name: "$exists", Value: false},
   159  				},
   160  			}},
   161  		},
   162  	}
   163  }