github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/resources/applier.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package resources 5 6 import ( 7 "context" 8 9 "github.com/juju/errors" 10 "github.com/juju/loggo" 11 "k8s.io/apimachinery/pkg/types" 12 "k8s.io/client-go/kubernetes" 13 ) 14 15 var logger = loggo.GetLogger("juju.kubernetes.provider.resources") 16 17 var ( 18 errConflict = errors.New("resource version conflict") 19 ) 20 21 // preferedPatchStrategy is the default patch strategy used by Juju. 22 const preferedPatchStrategy = types.StrategicMergePatchType 23 24 type applier struct { 25 ops []operation 26 } 27 28 // NewApplier creates a new applier. 29 func NewApplier() Applier { 30 return &applier{} 31 } 32 33 type opType int 34 35 const ( 36 opApply opType = iota 37 opDelete 38 ) 39 40 type operation struct { 41 opType 42 resource Resource 43 } 44 45 func (op *operation) process(ctx context.Context, api kubernetes.Interface, rollback Applier) error { 46 existingRes := op.resource.Clone() 47 // TODO: consider to `list` using label selectors instead of `get` by `name`. 48 // Because it's not good for non namespaced resources. 49 err := existingRes.Get(ctx, api) 50 found := true 51 if errors.IsNotFound(err) { 52 found = false 53 } else if err != nil { 54 return errors.Annotatef(err, "checking if resource %q exists or not", existingRes) 55 } 56 if found { 57 ver := op.resource.GetObjectMeta().GetResourceVersion() 58 if ver != "" && ver != existingRes.GetObjectMeta().GetResourceVersion() { 59 id := op.resource.ID() 60 return errors.Annotatef(errConflict, "%s %s", id.Type, id.Name) 61 } 62 } 63 switch op.opType { 64 case opApply: 65 err = op.resource.Apply(ctx, api) 66 if found { 67 // apply the previously existing resource. 68 rollback.Apply(existingRes) 69 } else { 70 // delete the new resource just created. 71 rollback.Delete(op.resource) 72 } 73 case opDelete: 74 err = op.resource.Delete(ctx, api) 75 if found { 76 rollback.Apply(existingRes) 77 } 78 } 79 return errors.Trace(err) 80 } 81 82 func (a *applier) Apply(resources ...Resource) { 83 for _, r := range resources { 84 a.ops = append(a.ops, operation{opApply, r}) 85 } 86 } 87 88 func (a *applier) Delete(resources ...Resource) { 89 for _, r := range resources { 90 a.ops = append(a.ops, operation{opDelete, r}) 91 } 92 } 93 94 func (a *applier) ApplySet(current []Resource, desired []Resource) { 95 desiredMap := map[ID]bool{} 96 for _, r := range desired { 97 desiredMap[r.ID()] = true 98 } 99 for _, r := range current { 100 if ok := desiredMap[r.ID()]; !ok { 101 a.Delete(r) 102 } 103 } 104 a.Apply(desired...) 105 } 106 107 func (a *applier) Run(ctx context.Context, client kubernetes.Interface, noRollback bool) (err error) { 108 rollback := NewApplier() 109 110 defer func() { 111 if noRollback || err == nil { 112 return 113 } 114 if rollbackErr := rollback.Run(ctx, client, true); rollbackErr != nil { 115 logger.Warningf("rollback failed %s", rollbackErr.Error()) 116 } 117 }() 118 for _, op := range a.ops { 119 if err = op.process(ctx, client, rollback); err != nil { 120 return errors.Trace(err) 121 } 122 } 123 return nil 124 }