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 }