github.com/argoproj/argo-cd/v3@v3.2.1/util/argo/normalizers/diff_normalizer_test.go (about)

     1  package normalizers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    11  	"sigs.k8s.io/yaml"
    12  
    13  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    14  	"github.com/argoproj/argo-cd/v3/test"
    15  )
    16  
    17  func TestNormalizeObjectWithMatchedGroupKind(t *testing.T) {
    18  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
    19  		Group:        "apps",
    20  		Kind:         "Deployment",
    21  		JSONPointers: []string{"/not-matching-path", "/spec/template/spec/containers"},
    22  	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
    23  
    24  	require.NoError(t, err)
    25  
    26  	deployment := test.NewDeployment()
    27  
    28  	_, has, err := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
    29  	require.NoError(t, err)
    30  	assert.True(t, has)
    31  
    32  	err = normalizer.Normalize(deployment)
    33  	require.NoError(t, err)
    34  	_, has, err = unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
    35  	require.NoError(t, err)
    36  	assert.False(t, has)
    37  
    38  	err = normalizer.Normalize(nil)
    39  	require.Error(t, err)
    40  }
    41  
    42  func TestNormalizeNoMatchedGroupKinds(t *testing.T) {
    43  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
    44  		Group:        "",
    45  		Kind:         "Service",
    46  		JSONPointers: []string{"/spec"},
    47  	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
    48  
    49  	require.NoError(t, err)
    50  
    51  	deployment := test.NewDeployment()
    52  
    53  	err = normalizer.Normalize(deployment)
    54  	require.NoError(t, err)
    55  
    56  	_, hasSpec, err := unstructured.NestedMap(deployment.Object, "spec")
    57  	require.NoError(t, err)
    58  	assert.True(t, hasSpec)
    59  }
    60  
    61  func TestNormalizeMatchedResourceOverrides(t *testing.T) {
    62  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
    63  		"apps/Deployment": {
    64  			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
    65  		},
    66  	}, IgnoreNormalizerOpts{})
    67  
    68  	require.NoError(t, err)
    69  
    70  	deployment := test.NewDeployment()
    71  
    72  	_, has, err := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
    73  	require.NoError(t, err)
    74  	assert.True(t, has)
    75  
    76  	err = normalizer.Normalize(deployment)
    77  	require.NoError(t, err)
    78  	_, has, err = unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
    79  	require.NoError(t, err)
    80  	assert.False(t, has)
    81  }
    82  
    83  const testCRDYAML = `
    84  apiVersion: apiextensions.k8s.io/v1
    85  kind: CustomResourceDefinition
    86  metadata:
    87    name: certificates.cert-manager.io
    88  spec:
    89    conversion:
    90      strategy: None
    91    group: cert-manager.io
    92    names:
    93      kind: Certificate
    94      listKind: CertificateList
    95      plural: certificates
    96      shortNames:
    97      - cert
    98      - certs
    99      singular: certificate
   100    scope: Namespaced
   101    version: v1alpha1
   102    versions:
   103    - name: v1alpha1
   104      served: true
   105      storage: true
   106      schema:
   107        openAPIV3Schema:
   108          type: object
   109          properties:
   110            json:
   111              x-kubernetes-preserve-unknown-fields: true`
   112  
   113  func TestNormalizeMissingJsonPointer(t *testing.T) {
   114  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
   115  		"apps/Deployment": {
   116  			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/garbage"}},
   117  		},
   118  		"apiextensions.k8s.io/CustomResourceDefinition": {
   119  			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/additionalPrinterColumns/0/priority"}},
   120  		},
   121  	}, IgnoreNormalizerOpts{})
   122  	require.NoError(t, err)
   123  
   124  	deployment := test.NewDeployment()
   125  
   126  	err = normalizer.Normalize(deployment)
   127  	require.NoError(t, err)
   128  
   129  	crd := unstructured.Unstructured{}
   130  	err = yaml.Unmarshal([]byte(testCRDYAML), &crd)
   131  	require.NoError(t, err)
   132  
   133  	err = normalizer.Normalize(&crd)
   134  	require.NoError(t, err)
   135  }
   136  
   137  func TestNormalizeGlobMatch(t *testing.T) {
   138  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
   139  		"*/*": {
   140  			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
   141  		},
   142  	}, IgnoreNormalizerOpts{})
   143  
   144  	require.NoError(t, err)
   145  
   146  	deployment := test.NewDeployment()
   147  
   148  	_, has, err := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
   149  	require.NoError(t, err)
   150  	assert.True(t, has)
   151  
   152  	err = normalizer.Normalize(deployment)
   153  	require.NoError(t, err)
   154  	_, has, err = unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
   155  	require.NoError(t, err)
   156  	assert.False(t, has)
   157  }
   158  
   159  func TestNormalizeJQPathExpression(t *testing.T) {
   160  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
   161  		Group:             "apps",
   162  		Kind:              "Deployment",
   163  		JQPathExpressions: []string{".spec.template.spec.initContainers[] | select(.name == \"init-container-0\")"},
   164  	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
   165  
   166  	require.NoError(t, err)
   167  
   168  	deployment := test.NewDeployment()
   169  
   170  	var initContainers []any
   171  	initContainers = append(initContainers, map[string]any{"name": "init-container-0"})
   172  	initContainers = append(initContainers, map[string]any{"name": "init-container-1"})
   173  	err = unstructured.SetNestedSlice(deployment.Object, initContainers, "spec", "template", "spec", "initContainers")
   174  	require.NoError(t, err)
   175  
   176  	actualInitContainers, has, err := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "initContainers")
   177  	require.NoError(t, err)
   178  	assert.True(t, has)
   179  	assert.Len(t, actualInitContainers, 2)
   180  
   181  	err = normalizer.Normalize(deployment)
   182  	require.NoError(t, err)
   183  	actualInitContainers, has, err = unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "initContainers")
   184  	require.NoError(t, err)
   185  	assert.True(t, has)
   186  	assert.Len(t, actualInitContainers, 1)
   187  
   188  	actualInitContainerName, has, err := unstructured.NestedString(actualInitContainers[0].(map[string]any), "name")
   189  	require.NoError(t, err)
   190  	assert.True(t, has)
   191  	assert.Equal(t, "init-container-1", actualInitContainerName)
   192  }
   193  
   194  func TestNormalizeIllegalJQPathExpression(t *testing.T) {
   195  	_, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
   196  		Group:             "apps",
   197  		Kind:              "Deployment",
   198  		JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"missing-quote)"},
   199  		// JSONPointers: []string{"no-starting-slash"},
   200  	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
   201  
   202  	require.Error(t, err)
   203  }
   204  
   205  func TestNormalizeJQPathExpressionWithError(t *testing.T) {
   206  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
   207  		Group:             "apps",
   208  		Kind:              "Deployment",
   209  		JQPathExpressions: []string{".spec.fakeField.foo[]"},
   210  	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
   211  
   212  	require.NoError(t, err)
   213  
   214  	deployment := test.NewDeployment()
   215  	originalDeployment, err := deployment.MarshalJSON()
   216  	require.NoError(t, err)
   217  
   218  	err = normalizer.Normalize(deployment)
   219  	require.NoError(t, err)
   220  
   221  	normalizedDeployment, err := deployment.MarshalJSON()
   222  	require.NoError(t, err)
   223  	assert.Equal(t, originalDeployment, normalizedDeployment)
   224  }
   225  
   226  func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
   227  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
   228  		"*/*": {
   229  			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
   230  				JSONPointers: []string{"/invalid", "/invalid/json/path"},
   231  			},
   232  		},
   233  	}, IgnoreNormalizerOpts{})
   234  	require.NoError(t, err)
   235  
   236  	ignoreNormalizer := normalizer.(*ignoreNormalizer)
   237  	assert.Len(t, ignoreNormalizer.patches, 2)
   238  	jsonPatch := ignoreNormalizer.patches[0]
   239  	jqPatch := ignoreNormalizer.patches[1]
   240  
   241  	deployment := test.NewDeployment()
   242  	deploymentData, err := json.Marshal(deployment)
   243  	require.NoError(t, err)
   244  
   245  	// Error: "error in remove for path: '/invalid': Unable to remove nonexistent key: invalid: missing value"
   246  	_, err = jsonPatch.Apply(deploymentData)
   247  	assert.False(t, shouldLogError(err))
   248  
   249  	// Error: "remove operation does not apply: doc is missing path: \"/invalid/json/path\": missing value"
   250  	_, err = jqPatch.Apply(deploymentData)
   251  	assert.False(t, shouldLogError(err))
   252  
   253  	assert.True(t, shouldLogError(errors.New("An error that should not be ignored")))
   254  }
   255  
   256  func TestJqPathExpressionFailWithTimeout(t *testing.T) {
   257  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
   258  		"*/*": {
   259  			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
   260  				JQPathExpressions: []string{"until(true==false; [.] + [1])"},
   261  			},
   262  		},
   263  	}, IgnoreNormalizerOpts{})
   264  	require.NoError(t, err)
   265  
   266  	ignoreNormalizer := normalizer.(*ignoreNormalizer)
   267  	assert.Len(t, ignoreNormalizer.patches, 1)
   268  	jqPatch := ignoreNormalizer.patches[0]
   269  
   270  	deployment := test.NewDeployment()
   271  	deploymentData, err := json.Marshal(deployment)
   272  	require.NoError(t, err)
   273  
   274  	_, err = jqPatch.Apply(deploymentData)
   275  	assert.ErrorContains(t, err, "JQ patch execution timed out")
   276  }
   277  
   278  func TestJQPathExpressionReturnsHelpfulError(t *testing.T) {
   279  	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
   280  		Kind: "ConfigMap",
   281  		// This is a really wild expression, but it does trigger the desired error.
   282  		JQPathExpressions: []string{`.nothing) | .data["config.yaml"] |= (fromjson | del(.auth) | tojson`},
   283  	}}, nil, IgnoreNormalizerOpts{})
   284  
   285  	require.NoError(t, err)
   286  
   287  	configMap := test.NewConfigMap()
   288  	require.NoError(t, err)
   289  
   290  	out := test.CaptureLogEntries(func() {
   291  		err = normalizer.Normalize(configMap)
   292  		require.NoError(t, err)
   293  	})
   294  	assert.Contains(t, out, "fromjson cannot be applied")
   295  }