github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/utils/createOrUpdate.go (about) 1 package utils 2 3 import ( 4 "context" 5 "encoding/json" 6 stderrors "errors" 7 "fmt" 8 9 log "github.com/sirupsen/logrus" 10 "k8s.io/apimachinery/pkg/api/errors" 11 "k8s.io/apimachinery/pkg/api/resource" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 "k8s.io/apimachinery/pkg/conversion" 15 "k8s.io/apimachinery/pkg/fields" 16 "k8s.io/apimachinery/pkg/labels" 17 "k8s.io/apimachinery/pkg/runtime" 18 "sigs.k8s.io/controller-runtime/pkg/client" 19 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 20 21 argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 22 "github.com/argoproj/argo-cd/v3/util/argo" 23 argodiff "github.com/argoproj/argo-cd/v3/util/argo/diff" 24 "github.com/argoproj/argo-cd/v3/util/argo/normalizers" 25 ) 26 27 // CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function 28 // in sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go 29 // to add equality for argov1alpha1.ApplicationDestination 30 // argov1alpha1.ApplicationDestination has a private variable, so the default 31 // implementation fails to compare it. 32 // 33 // CreateOrUpdate creates or updates the given object in the Kubernetes 34 // cluster. The object's desired state must be reconciled with the existing 35 // state inside the passed in callback MutateFn. 36 // 37 // The MutateFn is called regardless of creating or updating an object. 38 // 39 // It returns the executed operation and an error. 40 func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ignoreAppDifferences argov1alpha1.ApplicationSetIgnoreDifferences, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, obj *argov1alpha1.Application, f controllerutil.MutateFn) (controllerutil.OperationResult, error) { 41 key := client.ObjectKeyFromObject(obj) 42 if err := c.Get(ctx, key, obj); err != nil { 43 if !errors.IsNotFound(err) { 44 return controllerutil.OperationResultNone, err 45 } 46 if err := mutate(f, key, obj); err != nil { 47 return controllerutil.OperationResultNone, err 48 } 49 if err := c.Create(ctx, obj); err != nil { 50 return controllerutil.OperationResultNone, err 51 } 52 return controllerutil.OperationResultCreated, nil 53 } 54 55 normalizedLive := obj.DeepCopy() 56 57 // Mutate the live object to match the desired state. 58 if err := mutate(f, key, obj); err != nil { 59 return controllerutil.OperationResultNone, err 60 } 61 62 // Apply ignoreApplicationDifferences rules to remove ignored fields from both the live and the desired state. This 63 // prevents those differences from appearing in the diff and therefore in the patch. 64 err := applyIgnoreDifferences(ignoreAppDifferences, normalizedLive, obj, ignoreNormalizerOpts) 65 if err != nil { 66 return controllerutil.OperationResultNone, fmt.Errorf("failed to apply ignore differences: %w", err) 67 } 68 69 // Normalize to avoid diffing on unimportant differences. 70 normalizedLive.Spec = *argo.NormalizeApplicationSpec(&normalizedLive.Spec) 71 obj.Spec = *argo.NormalizeApplicationSpec(&obj.Spec) 72 73 equality := conversion.EqualitiesOrDie( 74 func(a, b resource.Quantity) bool { 75 // Ignore formatting, only care that numeric value stayed the same. 76 // TODO: if we decide it's important, it should be safe to start comparing the format. 77 // 78 // Uninitialized quantities are equivalent to 0 quantities. 79 return a.Cmp(b) == 0 80 }, 81 func(a, b metav1.MicroTime) bool { 82 return a.UTC().Equal(b.UTC()) 83 }, 84 func(a, b metav1.Time) bool { 85 return a.UTC().Equal(b.UTC()) 86 }, 87 func(a, b labels.Selector) bool { 88 return a.String() == b.String() 89 }, 90 func(a, b fields.Selector) bool { 91 return a.String() == b.String() 92 }, 93 func(a, b argov1alpha1.ApplicationDestination) bool { 94 return a.Namespace == b.Namespace && a.Name == b.Name && a.Server == b.Server 95 }, 96 ) 97 98 if equality.DeepEqual(normalizedLive, obj) { 99 return controllerutil.OperationResultNone, nil 100 } 101 102 patch := client.MergeFrom(normalizedLive) 103 if log.IsLevelEnabled(log.DebugLevel) { 104 LogPatch(logCtx, patch, obj) 105 } 106 if err := c.Patch(ctx, obj, patch); err != nil { 107 return controllerutil.OperationResultNone, err 108 } 109 return controllerutil.OperationResultUpdated, nil 110 } 111 112 func LogPatch(logCtx *log.Entry, patch client.Patch, obj *argov1alpha1.Application) { 113 patchBytes, err := patch.Data(obj) 114 if err != nil { 115 logCtx.Errorf("failed to generate patch: %v", err) 116 } 117 // Get the patch as a plain object so it is easier to work with in json logs. 118 var patchObj map[string]any 119 err = json.Unmarshal(patchBytes, &patchObj) 120 if err != nil { 121 logCtx.Errorf("failed to unmarshal patch: %v", err) 122 } 123 logCtx.WithField("patch", patchObj).Debug("patching application") 124 } 125 126 // mutate wraps a MutateFn and applies validation to its result 127 func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object) error { 128 if err := f(); err != nil { 129 return fmt.Errorf("error while wrapping using MutateFn: %w", err) 130 } 131 if newKey := client.ObjectKeyFromObject(obj); key != newKey { 132 return stderrors.New("MutateFn cannot mutate object name and/or object namespace") 133 } 134 return nil 135 } 136 137 // applyIgnoreDifferences applies the ignore differences rules to the found application. It modifies the applications in place. 138 func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp *argov1alpha1.Application, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) error { 139 if len(applicationSetIgnoreDifferences) == 0 { 140 return nil 141 } 142 143 generatedAppCopy := generatedApp.DeepCopy() 144 diffConfig, err := argodiff.NewDiffConfigBuilder(). 145 WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false, ignoreNormalizerOpts). 146 WithNoCache(). 147 Build() 148 if err != nil { 149 return fmt.Errorf("failed to build diff config: %w", err) 150 } 151 unstructuredFound, err := appToUnstructured(found) 152 if err != nil { 153 return fmt.Errorf("failed to convert found application to unstructured: %w", err) 154 } 155 unstructuredGenerated, err := appToUnstructured(generatedApp) 156 if err != nil { 157 return fmt.Errorf("failed to convert found application to unstructured: %w", err) 158 } 159 result, err := argodiff.Normalize([]*unstructured.Unstructured{unstructuredFound}, []*unstructured.Unstructured{unstructuredGenerated}, diffConfig) 160 if err != nil { 161 return fmt.Errorf("failed to normalize application spec: %w", err) 162 } 163 if len(result.Lives) != 1 { 164 return fmt.Errorf("expected 1 normalized application, got %d", len(result.Lives)) 165 } 166 foundJSONNormalized, err := json.Marshal(result.Lives[0].Object) 167 if err != nil { 168 return fmt.Errorf("failed to marshal normalized app to json: %w", err) 169 } 170 foundNormalized := &argov1alpha1.Application{} 171 err = json.Unmarshal(foundJSONNormalized, &foundNormalized) 172 if err != nil { 173 return fmt.Errorf("failed to unmarshal normalized app to json: %w", err) 174 } 175 if len(result.Targets) != 1 { 176 return fmt.Errorf("expected 1 normalized application, got %d", len(result.Targets)) 177 } 178 foundNormalized.DeepCopyInto(found) 179 generatedJSONNormalized, err := json.Marshal(result.Targets[0].Object) 180 if err != nil { 181 return fmt.Errorf("failed to marshal normalized app to json: %w", err) 182 } 183 generatedAppNormalized := &argov1alpha1.Application{} 184 err = json.Unmarshal(generatedJSONNormalized, &generatedAppNormalized) 185 if err != nil { 186 return fmt.Errorf("failed to unmarshal normalized app json to structured app: %w", err) 187 } 188 generatedAppNormalized.DeepCopyInto(generatedApp) 189 // Prohibit jq queries from mutating silly things. 190 generatedApp.TypeMeta = generatedAppCopy.TypeMeta 191 generatedApp.Name = generatedAppCopy.Name 192 generatedApp.Namespace = generatedAppCopy.Namespace 193 generatedApp.Operation = generatedAppCopy.Operation 194 return nil 195 } 196 197 func appToUnstructured(app client.Object) (*unstructured.Unstructured, error) { 198 u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(app) 199 if err != nil { 200 return nil, fmt.Errorf("failed to convert app object to unstructured: %w", err) 201 } 202 return &unstructured.Unstructured{Object: u}, nil 203 }