github.com/argoproj/argo-cd/v3@v3.2.1/util/argo/normalizers/diff_normalizer.go (about) 1 package normalizers 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/argoproj/gitops-engine/pkg/diff" 12 jsonpatch "github.com/evanphx/json-patch" 13 "github.com/itchyny/gojq" 14 log "github.com/sirupsen/logrus" 15 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 "k8s.io/apimachinery/pkg/runtime/schema" 17 18 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 19 "github.com/argoproj/argo-cd/v3/util/glob" 20 ) 21 22 const ( 23 // DefaultJQExecutionTimeout is the maximum time allowed for a JQ patch to execute 24 DefaultJQExecutionTimeout = 1 * time.Second 25 ) 26 27 type normalizerPatch interface { 28 GetGroupKind() schema.GroupKind 29 GetNamespace() string 30 GetName() string 31 // Apply(un *unstructured.Unstructured) (error) 32 Apply(data []byte) ([]byte, error) 33 } 34 35 type baseNormalizerPatch struct { 36 groupKind schema.GroupKind 37 namespace string 38 name string 39 } 40 41 func (np *baseNormalizerPatch) GetGroupKind() schema.GroupKind { 42 return np.groupKind 43 } 44 45 func (np *baseNormalizerPatch) GetNamespace() string { 46 return np.namespace 47 } 48 49 func (np *baseNormalizerPatch) GetName() string { 50 return np.name 51 } 52 53 type jsonPatchNormalizerPatch struct { 54 baseNormalizerPatch 55 patch *jsonpatch.Patch 56 } 57 58 func (np *jsonPatchNormalizerPatch) Apply(data []byte) ([]byte, error) { 59 patchedData, err := np.patch.Apply(data) 60 if err != nil { 61 return nil, err 62 } 63 return patchedData, nil 64 } 65 66 type jqNormalizerPatch struct { 67 baseNormalizerPatch 68 code *gojq.Code 69 jqExecutionTimeout time.Duration 70 } 71 72 func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) { 73 dataJSON := make(map[string]any) 74 err := json.Unmarshal(data, &dataJSON) 75 if err != nil { 76 return nil, err 77 } 78 79 ctx, cancel := context.WithTimeout(context.Background(), np.jqExecutionTimeout) 80 defer cancel() 81 82 iter := np.code.RunWithContext(ctx, dataJSON) 83 first, ok := iter.Next() 84 if !ok { 85 return nil, errors.New("JQ patch did not return any data") 86 } 87 if err, ok = first.(error); ok { 88 if errors.Is(err, context.DeadlineExceeded) { 89 return nil, fmt.Errorf("JQ patch execution timed out (%v)", np.jqExecutionTimeout.String()) 90 } 91 return nil, fmt.Errorf("JQ patch returned error: %w", err) 92 } 93 _, ok = iter.Next() 94 if ok { 95 return nil, errors.New("JQ patch returned multiple objects") 96 } 97 98 patchedData, err := json.Marshal(first) 99 if err != nil { 100 return nil, err 101 } 102 return patchedData, err 103 } 104 105 type ignoreNormalizer struct { 106 patches []normalizerPatch 107 } 108 109 type IgnoreNormalizerOpts struct { 110 JQExecutionTimeout time.Duration 111 } 112 113 func (opts *IgnoreNormalizerOpts) getJQExecutionTimeout() time.Duration { 114 if opts == nil || opts.JQExecutionTimeout == 0 { 115 return DefaultJQExecutionTimeout 116 } 117 return opts.JQExecutionTimeout 118 } 119 120 // NewIgnoreNormalizer creates diff normalizer which removes ignored fields according to given application spec and resource overrides 121 func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts IgnoreNormalizerOpts) (diff.Normalizer, error) { 122 for key, override := range overrides { 123 group, kind, err := getGroupKindForOverrideKey(key) 124 if err != nil { 125 log.Warn(err) 126 } 127 if len(override.IgnoreDifferences.JSONPointers) > 0 || len(override.IgnoreDifferences.JQPathExpressions) > 0 { 128 resourceIgnoreDifference := v1alpha1.ResourceIgnoreDifferences{ 129 Group: group, 130 Kind: kind, 131 } 132 if len(override.IgnoreDifferences.JSONPointers) > 0 { 133 resourceIgnoreDifference.JSONPointers = override.IgnoreDifferences.JSONPointers 134 } 135 if len(override.IgnoreDifferences.JQPathExpressions) > 0 { 136 resourceIgnoreDifference.JQPathExpressions = override.IgnoreDifferences.JQPathExpressions 137 } 138 ignore = append(ignore, resourceIgnoreDifference) 139 } 140 } 141 patches := make([]normalizerPatch, 0) 142 for i := range ignore { 143 for _, path := range ignore[i].JSONPointers { 144 patchData, err := json.Marshal([]map[string]string{{"op": "remove", "path": path}}) 145 if err != nil { 146 return nil, err 147 } 148 patch, err := jsonpatch.DecodePatch(patchData) 149 if err != nil { 150 return nil, err 151 } 152 patches = append(patches, &jsonPatchNormalizerPatch{ 153 baseNormalizerPatch: baseNormalizerPatch{ 154 groupKind: schema.GroupKind{Group: ignore[i].Group, Kind: ignore[i].Kind}, 155 name: ignore[i].Name, 156 namespace: ignore[i].Namespace, 157 }, 158 patch: &patch, 159 }) 160 } 161 for _, pathExpression := range ignore[i].JQPathExpressions { 162 jqDeletionQuery, err := gojq.Parse(fmt.Sprintf("del(%s)", pathExpression)) 163 if err != nil { 164 return nil, err 165 } 166 jqDeletionCode, err := gojq.Compile(jqDeletionQuery) 167 if err != nil { 168 return nil, err 169 } 170 patches = append(patches, &jqNormalizerPatch{ 171 baseNormalizerPatch: baseNormalizerPatch{ 172 groupKind: schema.GroupKind{Group: ignore[i].Group, Kind: ignore[i].Kind}, 173 name: ignore[i].Name, 174 namespace: ignore[i].Namespace, 175 }, 176 code: jqDeletionCode, 177 jqExecutionTimeout: opts.getJQExecutionTimeout(), 178 }) 179 } 180 } 181 return &ignoreNormalizer{patches: patches}, nil 182 } 183 184 // Normalize removes fields from supplied resource using json paths from matching items of specified resources ignored differences list 185 func (n *ignoreNormalizer) Normalize(un *unstructured.Unstructured) error { 186 if un == nil { 187 return errors.New("invalid argument: unstructured is nil") 188 } 189 matched := make([]normalizerPatch, 0) 190 for _, patch := range n.patches { 191 groupKind := un.GroupVersionKind().GroupKind() 192 193 if glob.Match(patch.GetGroupKind().Group, groupKind.Group) && 194 glob.Match(patch.GetGroupKind().Kind, groupKind.Kind) && 195 (patch.GetName() == "" || patch.GetName() == un.GetName()) && 196 (patch.GetNamespace() == "" || patch.GetNamespace() == un.GetNamespace()) { 197 matched = append(matched, patch) 198 } 199 } 200 if len(matched) == 0 { 201 return nil 202 } 203 204 docData, err := json.Marshal(un) 205 if err != nil { 206 return err 207 } 208 209 for _, patch := range matched { 210 patchedDocData, err := patch.Apply(docData) 211 if err != nil { 212 if shouldLogError(err) { 213 log.Debugf("Failed to apply normalization: %v", err) 214 } 215 continue 216 } 217 docData = patchedDocData 218 } 219 220 err = json.Unmarshal(docData, un) 221 if err != nil { 222 return err 223 } 224 return nil 225 } 226 227 func shouldLogError(e error) bool { 228 if strings.Contains(e.Error(), "Unable to remove nonexistent key") { 229 return false 230 } 231 if strings.Contains(e.Error(), "remove operation does not apply: doc is missing path") { 232 return false 233 } 234 return true 235 }