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  }