github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/backup_test.go (about)

     1  package admin
     2  
     3  import (
     4  	"bytes"
     5  	"testing"
     6  
     7  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
     8  	"github.com/argoproj/argo-cd/v3/util/security"
     9  
    10  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    11  	"github.com/stretchr/testify/assert"
    12  	corev1 "k8s.io/api/core/v1"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  
    16  	"github.com/argoproj/argo-cd/v3/common"
    17  )
    18  
    19  func newBackupObject(trackingValue string, trackingLabel bool, trackingAnnotation bool) *unstructured.Unstructured {
    20  	cm := corev1.ConfigMap{
    21  		ObjectMeta: metav1.ObjectMeta{
    22  			Name:      "my-configmap",
    23  			Namespace: "namespace",
    24  		},
    25  		Data: map[string]string{
    26  			"foo": "bar",
    27  		},
    28  	}
    29  	if trackingLabel {
    30  		cm.SetLabels(map[string]string{
    31  			common.LabelKeyAppInstance: trackingValue,
    32  		})
    33  	}
    34  	if trackingAnnotation {
    35  		cm.SetAnnotations(map[string]string{
    36  			common.AnnotationKeyAppInstance: trackingValue,
    37  		})
    38  	}
    39  	return kube.MustToUnstructured(&cm)
    40  }
    41  
    42  func newConfigmapObject() *unstructured.Unstructured {
    43  	cm := corev1.ConfigMap{
    44  		ObjectMeta: metav1.ObjectMeta{
    45  			Name:      common.ArgoCDConfigMapName,
    46  			Namespace: "argocd",
    47  			Labels: map[string]string{
    48  				"app.kubernetes.io/part-of": "argocd",
    49  			},
    50  		},
    51  	}
    52  
    53  	return kube.MustToUnstructured(&cm)
    54  }
    55  
    56  func newSecretsObject() *unstructured.Unstructured {
    57  	secret := corev1.Secret{
    58  		ObjectMeta: metav1.ObjectMeta{
    59  			Name:      common.ArgoCDSecretName,
    60  			Namespace: "default",
    61  			Labels: map[string]string{
    62  				"app.kubernetes.io/part-of": "argocd",
    63  			},
    64  		},
    65  		Data: map[string][]byte{
    66  			"admin.password":   nil,
    67  			"server.secretkey": nil,
    68  		},
    69  	}
    70  
    71  	return kube.MustToUnstructured(&secret)
    72  }
    73  
    74  func newAppProject() *unstructured.Unstructured {
    75  	appProject := v1alpha1.AppProject{
    76  		ObjectMeta: metav1.ObjectMeta{
    77  			Name:      "default",
    78  			Namespace: "argocd",
    79  		},
    80  		Spec: v1alpha1.AppProjectSpec{
    81  			Destinations: []v1alpha1.ApplicationDestination{
    82  				{
    83  					Namespace: "*",
    84  					Server:    "*",
    85  				},
    86  			},
    87  			ClusterResourceWhitelist: []metav1.GroupKind{
    88  				{
    89  					Group: "*",
    90  					Kind:  "*",
    91  				},
    92  			},
    93  			SourceRepos: []string{"*"},
    94  		},
    95  	}
    96  
    97  	return kube.MustToUnstructured(&appProject)
    98  }
    99  
   100  func newApplication(namespace string) *unstructured.Unstructured {
   101  	app := v1alpha1.Application{
   102  		TypeMeta: metav1.TypeMeta{
   103  			Kind: "Application",
   104  		},
   105  		ObjectMeta: metav1.ObjectMeta{
   106  			Name:      "test",
   107  			Namespace: namespace,
   108  		},
   109  		Spec: v1alpha1.ApplicationSpec{
   110  			Source:  &v1alpha1.ApplicationSource{},
   111  			Project: "default",
   112  			Destination: v1alpha1.ApplicationDestination{
   113  				Server:    v1alpha1.KubernetesInternalAPIServerAddr,
   114  				Namespace: "default",
   115  			},
   116  		},
   117  	}
   118  
   119  	return kube.MustToUnstructured(&app)
   120  }
   121  
   122  func newApplicationSet(namespace string) *unstructured.Unstructured {
   123  	appSet := v1alpha1.ApplicationSet{
   124  		TypeMeta: metav1.TypeMeta{
   125  			Kind: "ApplicationSet",
   126  		},
   127  		ObjectMeta: metav1.ObjectMeta{
   128  			Name:      "test-appset",
   129  			Namespace: namespace,
   130  		},
   131  		Spec: v1alpha1.ApplicationSetSpec{
   132  			Generators: []v1alpha1.ApplicationSetGenerator{
   133  				{
   134  					Git: &v1alpha1.GitGenerator{
   135  						RepoURL: "https://github.com/org/repo",
   136  					},
   137  				},
   138  			},
   139  		},
   140  	}
   141  
   142  	return kube.MustToUnstructured(&appSet)
   143  }
   144  
   145  // Test_exportResources tests for the resources exported when using the `argocd admin export` command
   146  func Test_exportResources(t *testing.T) {
   147  	tests := []struct {
   148  		name                string
   149  		object              *unstructured.Unstructured
   150  		namespace           string
   151  		enabledNamespaces   []string
   152  		expectedFileContent string
   153  		expectExport        bool
   154  	}{
   155  		{
   156  			name:         "ConfigMap should be in the exported manifest",
   157  			object:       newConfigmapObject(),
   158  			expectExport: true,
   159  			expectedFileContent: `apiVersion: ""
   160  kind: ""
   161  metadata:
   162    labels:
   163      app.kubernetes.io/part-of: argocd
   164    name: argocd-cm
   165  ---
   166  `,
   167  		},
   168  		{
   169  			name:         "Secret should be in the exported manifest",
   170  			object:       newSecretsObject(),
   171  			expectExport: true,
   172  			expectedFileContent: `apiVersion: ""
   173  data:
   174    admin.password: null
   175    server.secretkey: null
   176  kind: ""
   177  metadata:
   178    labels:
   179      app.kubernetes.io/part-of: argocd
   180    name: argocd-secret
   181    namespace: default
   182  ---
   183  `,
   184  		},
   185  		{
   186  			name:         "App Project should be in the exported manifest",
   187  			object:       newAppProject(),
   188  			expectExport: true,
   189  			expectedFileContent: `apiVersion: ""
   190  kind: ""
   191  metadata:
   192    name: default
   193  spec:
   194    clusterResourceWhitelist:
   195    - group: '*'
   196      kind: '*'
   197    destinations:
   198    - namespace: '*'
   199      server: '*'
   200    sourceRepos:
   201    - '*'
   202  status: {}
   203  ---
   204  `,
   205  		},
   206  		{
   207  			name:         "Application should be in the exported manifest when created in the default 'argocd' namespace",
   208  			object:       newApplication("argocd"),
   209  			namespace:    "argocd",
   210  			expectExport: true,
   211  			expectedFileContent: `apiVersion: ""
   212  kind: Application
   213  metadata:
   214    name: test
   215  spec:
   216    destination:
   217      namespace: default
   218      server: https://kubernetes.default.svc
   219    project: default
   220    source:
   221      repoURL: ""
   222  status:
   223    health: {}
   224    sourceHydrator: {}
   225    summary: {}
   226    sync:
   227      comparedTo:
   228        destination: {}
   229        source:
   230          repoURL: ""
   231      status: ""
   232  ---
   233  `,
   234  		},
   235  		{
   236  			name:              "Application should be in the exported manifest when created in the enabled namespaces",
   237  			object:            newApplication("dev"),
   238  			namespace:         "dev",
   239  			enabledNamespaces: []string{"dev", "prod"},
   240  			expectExport:      true,
   241  			expectedFileContent: `apiVersion: ""
   242  kind: Application
   243  metadata:
   244    name: test
   245    namespace: dev
   246  spec:
   247    destination:
   248      namespace: default
   249      server: https://kubernetes.default.svc
   250    project: default
   251    source:
   252      repoURL: ""
   253  status:
   254    health: {}
   255    sourceHydrator: {}
   256    summary: {}
   257    sync:
   258      comparedTo:
   259        destination: {}
   260        source:
   261          repoURL: ""
   262      status: ""
   263  ---
   264  `,
   265  		},
   266  		{
   267  			name:                "Application should not be in the exported manifest when it's neither created in the default argod namespace nor in enabled namespace",
   268  			object:              newApplication("staging"),
   269  			namespace:           "staging",
   270  			enabledNamespaces:   []string{"dev", "prod"},
   271  			expectExport:        false,
   272  			expectedFileContent: ``,
   273  		},
   274  		{
   275  			name:         "ApplicationSet should be in the exported manifest when created in the default 'argocd' namespace",
   276  			object:       newApplicationSet("argocd"),
   277  			namespace:    "argocd",
   278  			expectExport: true,
   279  			expectedFileContent: `apiVersion: ""
   280  kind: ApplicationSet
   281  metadata:
   282    name: test-appset
   283  spec:
   284    generators:
   285    - git:
   286        repoURL: https://github.com/org/repo
   287        revision: ""
   288        template:
   289          metadata: {}
   290          spec:
   291            destination: {}
   292            project: ""
   293    template:
   294      metadata: {}
   295      spec:
   296        destination: {}
   297        project: ""
   298  status: {}
   299  ---
   300  `,
   301  		},
   302  		{
   303  			name:              "ApplicationSet should be in the exported manifest when created in the enabled namespaces",
   304  			object:            newApplicationSet("dev"),
   305  			namespace:         "dev",
   306  			enabledNamespaces: []string{"dev", "prod"},
   307  			expectExport:      true,
   308  			expectedFileContent: `apiVersion: ""
   309  kind: ApplicationSet
   310  metadata:
   311    name: test-appset
   312    namespace: dev
   313  spec:
   314    generators:
   315    - git:
   316        repoURL: https://github.com/org/repo
   317        revision: ""
   318        template:
   319          metadata: {}
   320          spec:
   321            destination: {}
   322            project: ""
   323    template:
   324      metadata: {}
   325      spec:
   326        destination: {}
   327        project: ""
   328  status: {}
   329  ---
   330  `,
   331  		},
   332  		{
   333  			name:                "ApplicationSet should not be in the exported manifest when neither created in the default 'argocd' namespace nor in enabled namespaces",
   334  			object:              newApplicationSet("staging"),
   335  			namespace:           "staging",
   336  			enabledNamespaces:   []string{"dev", "prod"},
   337  			expectExport:        false,
   338  			expectedFileContent: ``,
   339  		},
   340  	}
   341  
   342  	for _, tt := range tests {
   343  		t.Run(tt.name, func(t *testing.T) {
   344  			var buf bytes.Buffer
   345  
   346  			kind := tt.object.GetKind()
   347  			if kind == "Application" || kind == "ApplicationSet" {
   348  				if security.IsNamespaceEnabled(tt.namespace, "argocd", tt.enabledNamespaces) {
   349  					export(&buf, *tt.object, ArgoCDNamespace)
   350  				}
   351  			} else {
   352  				export(&buf, *tt.object, ArgoCDNamespace)
   353  			}
   354  
   355  			content := buf.String()
   356  			if tt.expectExport {
   357  				assert.Equal(t, tt.expectedFileContent, content)
   358  			} else {
   359  				assert.Empty(t, content)
   360  			}
   361  		})
   362  	}
   363  }
   364  
   365  func Test_updateTracking(t *testing.T) {
   366  	type args struct {
   367  		bak  *unstructured.Unstructured
   368  		live *unstructured.Unstructured
   369  	}
   370  	tests := []struct {
   371  		name     string
   372  		args     args
   373  		expected *unstructured.Unstructured
   374  	}{
   375  		{
   376  			name: "update annotation when present in live",
   377  			args: args{
   378  				bak:  newBackupObject("bak", false, true),
   379  				live: newBackupObject("live", false, true),
   380  			},
   381  			expected: newBackupObject("live", false, true),
   382  		},
   383  		{
   384  			name: "update default label when present in live",
   385  			args: args{
   386  				bak:  newBackupObject("bak", true, true),
   387  				live: newBackupObject("live", true, true),
   388  			},
   389  			expected: newBackupObject("live", true, true),
   390  		},
   391  		{
   392  			name: "do not update if live object does not have tracking",
   393  			args: args{
   394  				bak:  newBackupObject("bak", true, true),
   395  				live: newBackupObject("live", false, false),
   396  			},
   397  			expected: newBackupObject("bak", true, true),
   398  		},
   399  		{
   400  			name: "do not update if bak object does not have tracking",
   401  			args: args{
   402  				bak:  newBackupObject("bak", false, false),
   403  				live: newBackupObject("live", true, true),
   404  			},
   405  			expected: newBackupObject("bak", false, false),
   406  		},
   407  	}
   408  	for _, tt := range tests {
   409  		t.Run(tt.name, func(t *testing.T) {
   410  			updateTracking(tt.args.bak, tt.args.live)
   411  			assert.Equal(t, tt.expected, tt.args.bak)
   412  		})
   413  	}
   414  }
   415  
   416  func TestIsSkipLabelMatches(t *testing.T) {
   417  	tests := []struct {
   418  		name       string
   419  		obj        *unstructured.Unstructured
   420  		skipLabels string
   421  		expected   bool
   422  	}{
   423  		{
   424  			name: "Label matches",
   425  			obj: &unstructured.Unstructured{
   426  				Object: map[string]any{
   427  					"metadata": map[string]any{
   428  						"labels": map[string]any{
   429  							"test-label": "value",
   430  						},
   431  					},
   432  				},
   433  			},
   434  			skipLabels: "test-label=value",
   435  			expected:   true,
   436  		},
   437  		{
   438  			name: "Label does not match",
   439  			obj: &unstructured.Unstructured{
   440  				Object: map[string]any{
   441  					"metadata": map[string]any{
   442  						"labels": map[string]any{
   443  							"different-label": "value",
   444  						},
   445  					},
   446  				},
   447  			},
   448  			skipLabels: "test-label=value",
   449  			expected:   false,
   450  		},
   451  		{
   452  			name: "Empty skip labels",
   453  			obj: &unstructured.Unstructured{
   454  				Object: map[string]any{
   455  					"metadata": map[string]any{
   456  						"labels": map[string]any{
   457  							"test-label": "value",
   458  						},
   459  					},
   460  				},
   461  			},
   462  			skipLabels: "",
   463  			expected:   false,
   464  		},
   465  		{
   466  			name: "No labels value",
   467  			obj: &unstructured.Unstructured{
   468  				Object: map[string]any{
   469  					"metadata": map[string]any{
   470  						"labels": map[string]any{
   471  							"test-label":    "value",
   472  							"another-label": "value2",
   473  						},
   474  					},
   475  				},
   476  			},
   477  			skipLabels: "test-label",
   478  			expected:   false,
   479  		},
   480  	}
   481  	for _, tt := range tests {
   482  		t.Run(tt.name, func(t *testing.T) {
   483  			result := isSkipLabelMatches(tt.obj, tt.skipLabels)
   484  			assert.Equal(t, tt.expected, result)
   485  		})
   486  	}
   487  }