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  }