package controller import ( "encoding/json" "strings" "testing" "time" "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" mockstatecache "github.com/argoproj/argo-cd/controller/cache/mocks" argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake" mockreposerver "github.com/argoproj/argo-cd/reposerver/mocks" "github.com/argoproj/argo-cd/reposerver/repository" mockrepoclient "github.com/argoproj/argo-cd/reposerver/repository/mocks" "github.com/argoproj/argo-cd/test" "github.com/argoproj/argo-cd/util/kube" ) type fakeData struct { apps []runtime.Object manifestResponse *repository.ManifestResponse managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured } func newFakeController(data *fakeData) *ApplicationController { var clust corev1.Secret err := yaml.Unmarshal([]byte(fakeCluster), &clust) if err != nil { panic(err) } // Mock out call to GenerateManifest mockRepoClient := mockrepoclient.RepositoryServiceClient{} mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil) mockRepoClientset := mockreposerver.Clientset{} mockRepoClientset.On("NewRepositoryClient").Return(&fakeCloser{}, &mockRepoClient, nil) secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "argocd-secret", Namespace: test.FakeArgoCDNamespace, }, Data: map[string][]byte{ "admin.password": []byte("test"), "server.secretkey": []byte("test"), }, } cm := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "argocd-cm", Namespace: test.FakeArgoCDNamespace, }, Data: nil, } ctrl := NewApplicationController( test.FakeArgoCDNamespace, fake.NewSimpleClientset(&clust, &cm, &secret), appclientset.NewSimpleClientset(data.apps...), &mockRepoClientset, time.Minute, ) // Mock out call to GetManagedLiveObjs if fake data supplied if data.managedLiveObjs != nil { mockStateCache := mockstatecache.LiveStateCache{} mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil) mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil) ctrl.stateCache = &mockStateCache ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache } return ctrl } type fakeCloser struct{} func (f *fakeCloser) Close() error { return nil } var fakeCluster = ` apiVersion: v1 data: # {"bearerToken":"fake","tlsClientConfig":{"insecure":true},"awsAuthConfig":null} config: eyJiZWFyZXJUb2tlbiI6ImZha2UiLCJ0bHNDbGllbnRDb25maWciOnsiaW5zZWN1cmUiOnRydWV9LCJhd3NBdXRoQ29uZmlnIjpudWxsfQ== # minikube name: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw== # https://localhost:6443 server: aHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3Zj kind: Secret metadata: labels: argocd.argoproj.io/secret-type: cluster name: localhost-6443 namespace: ` + test.FakeArgoCDNamespace + ` type: Opaque ` var fakeApp = ` apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: my-app namespace: ` + test.FakeArgoCDNamespace + ` spec: destination: namespace: ` + test.FakeDestNamespace + ` server: https://localhost:6443 project: default source: path: some/path repoURL: https://github.com/argoproj/argocd-example-apps.git syncPolicy: automated: {} status: operationState: finishedAt: 2018-09-21T23:50:29Z message: successfully synced operation: sync: revision: HEAD phase: Succeeded startedAt: 2018-09-21T23:50:25Z syncResult: resources: - kind: RoleBinding message: |- rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled rolebinding.rbac.authorization.k8s.io/always-outofsync configured name: always-outofsync namespace: default status: Synced revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ` func newFakeApp() *argoappv1.Application { var app argoappv1.Application err := yaml.Unmarshal([]byte(fakeApp), &app) if err != nil { panic(err) } return &app } func TestAutoSync(t *testing.T) { app := newFakeApp() ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus := argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeOutOfSync, Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } cond := ctrl.autoSync(app, &syncStatus) assert.Nil(t, cond) app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.NotNil(t, app.Operation) assert.NotNil(t, app.Operation.Sync) assert.False(t, app.Operation.Sync.Prune) } func TestSkipAutoSync(t *testing.T) { // Verify we skip when we previously synced to it in our most recent history // Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync app := newFakeApp() ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus := argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeOutOfSync, Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", } cond := ctrl.autoSync(app, &syncStatus) assert.Nil(t, cond) app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.Nil(t, app.Operation) // Verify we skip when we are already Synced (even if revision is different) app = newFakeApp() ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus = argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeSynced, Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } cond = ctrl.autoSync(app, &syncStatus) assert.Nil(t, cond) app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.Nil(t, app.Operation) // Verify we skip when auto-sync is disabled app = newFakeApp() app.Spec.SyncPolicy = nil ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus = argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeOutOfSync, Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } cond = ctrl.autoSync(app, &syncStatus) assert.Nil(t, cond) app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.Nil(t, app.Operation) // Verify we skip when previous sync attempt failed and return error condition // Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history app = newFakeApp() app.Status.OperationState = &argoappv1.OperationState{ Operation: argoappv1.Operation{ Sync: &argoappv1.SyncOperation{}, }, Phase: argoappv1.OperationFailed, SyncResult: &argoappv1.SyncOperationResult{ Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", }, } ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus = argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeOutOfSync, Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } cond = ctrl.autoSync(app, &syncStatus) assert.NotNil(t, cond) app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.Nil(t, app.Operation) } // TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed func TestAutoSyncIndicateError(t *testing.T) { app := newFakeApp() app.Spec.Source.ComponentParameterOverrides = []argoappv1.ComponentParameter{ { Name: "a", Value: "1", }, } ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus := argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeOutOfSync, Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", } app.Status.OperationState = &argoappv1.OperationState{ Operation: argoappv1.Operation{ Sync: &argoappv1.SyncOperation{ ParameterOverrides: argoappv1.ParameterOverrides{ { Name: "a", Value: "1", }, }, }, }, Phase: argoappv1.OperationFailed, SyncResult: &argoappv1.SyncOperationResult{ Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, } cond := ctrl.autoSync(app, &syncStatus) assert.NotNil(t, cond) app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.Nil(t, app.Operation) } // TestAutoSyncParameterOverrides verifies we auto-sync if revision is same but parameter overrides are different func TestAutoSyncParameterOverrides(t *testing.T) { app := newFakeApp() app.Spec.Source.ComponentParameterOverrides = []argoappv1.ComponentParameter{ { Name: "a", Value: "1", }, } ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) syncStatus := argoappv1.SyncStatus{ Status: argoappv1.SyncStatusCodeOutOfSync, Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", } app.Status.OperationState = &argoappv1.OperationState{ Operation: argoappv1.Operation{ Sync: &argoappv1.SyncOperation{ ParameterOverrides: argoappv1.ParameterOverrides{ { Name: "a", Value: "2", // this value changed }, }, }, }, Phase: argoappv1.OperationFailed, SyncResult: &argoappv1.SyncOperationResult{ Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, } cond := ctrl.autoSync(app, &syncStatus) assert.Nil(t, cond) app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{}) assert.NoError(t, err) assert.NotNil(t, app.Operation) } // TestFinalizeAppDeletion verifies application deletion func TestFinalizeAppDeletion(t *testing.T) { app := newFakeApp() ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) patched := false fakeAppCs.ReactionChain = nil fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { patched = true return true, nil, nil }) err := ctrl.finalizeApplicationDeletion(app) // TODO: use an interface to fake out the calls to GetResourcesWithLabel and DeleteResourceWithLabel // For now just ensure we have an expected error condition assert.Error(t, err) // Change this to assert.Nil when we stub out GetResourcesWithLabel/DeleteResourceWithLabel assert.False(t, patched) // Change this to assert.True when we stub out GetResourcesWithLabel/DeleteResourceWithLabel } // TestNormalizeApplication verifies we normalize an application during reconciliation func TestNormalizeApplication(t *testing.T) { app := newFakeApp() app.Spec.Project = "" ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) cancel := test.StartInformer(ctrl.appInformer) defer cancel() key, _ := cache.MetaNamespaceKeyFunc(app) ctrl.appRefreshQueue.Add(key) fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) fakeAppCs.ReactionChain = nil normalized := false fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { if patchAction, ok := action.(kubetesting.PatchAction); ok { if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` { normalized = true } } return true, nil, nil }) ctrl.processAppRefreshQueueItem() assert.True(t, normalized) } // TestDontNormalizeApplication verifies we dont unnecessarily normalize an application func TestDontNormalizeApplication(t *testing.T) { app := newFakeApp() app.Spec.Project = "default" ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}) cancel := test.StartInformer(ctrl.appInformer) defer cancel() key, _ := cache.MetaNamespaceKeyFunc(app) ctrl.appRefreshQueue.Add(key) fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) fakeAppCs.ReactionChain = nil normalized := false fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { if patchAction, ok := action.(kubetesting.PatchAction); ok { if strings.HasPrefix(string(patchAction.GetPatch()), `{"spec":`) { normalized = true } } return true, nil, nil }) ctrl.processAppRefreshQueueItem() assert.False(t, normalized) } func createSecret(data map[string]string) *unstructured.Unstructured { secret := corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: kube.SecretKind}} if data != nil { secret.Data = make(map[string][]byte) for k, v := range data { secret.Data[k] = []byte(v) } } return kube.MustToUnstructured(&secret) } func secretData(obj *unstructured.Unstructured) map[string]interface{} { data, _, _ := unstructured.NestedMap(obj.Object, "data") return data } const ( replacement1 = "*********" replacement2 = "**********" replacement3 = "***********" ) func TestHideSecretDataSameKeysDifferentValues(t *testing.T) { target, live, err := hideSecretData( createSecret(map[string]string{"key1": "test", "key2": "test"}), createSecret(map[string]string{"key1": "test-1", "key2": "test-1"})) assert.Nil(t, err) assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target)) assert.Equal(t, map[string]interface{}{"key1": replacement2, "key2": replacement2}, secretData(live)) } func TestHideSecretDataSameKeysSameValues(t *testing.T) { target, live, err := hideSecretData( createSecret(map[string]string{"key1": "test", "key2": "test"}), createSecret(map[string]string{"key1": "test", "key2": "test"})) assert.Nil(t, err) assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target)) assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(live)) } func TestHideSecretDataDifferentKeysDifferentValues(t *testing.T) { target, live, err := hideSecretData( createSecret(map[string]string{"key1": "test", "key2": "test"}), createSecret(map[string]string{"key2": "test-1", "key3": "test-1"})) assert.Nil(t, err) assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target)) assert.Equal(t, map[string]interface{}{"key2": replacement2, "key3": replacement1}, secretData(live)) } func TestHideSecretDataLastAppliedConfig(t *testing.T) { lastAppliedSecret := createSecret(map[string]string{"key1": "test1"}) targetSecret := createSecret(map[string]string{"key1": "test2"}) liveSecret := createSecret(map[string]string{"key1": "test3"}) lastAppliedStr, err := json.Marshal(lastAppliedSecret) assert.Nil(t, err) liveSecret.SetAnnotations(map[string]string{corev1.LastAppliedConfigAnnotation: string(lastAppliedStr)}) target, live, err := hideSecretData(targetSecret, liveSecret) assert.Nil(t, err) err = json.Unmarshal([]byte(live.GetAnnotations()[corev1.LastAppliedConfigAnnotation]), &lastAppliedSecret) assert.Nil(t, err) assert.Equal(t, map[string]interface{}{"key1": replacement1}, secretData(target)) assert.Equal(t, map[string]interface{}{"key1": replacement2}, secretData(live)) assert.Equal(t, map[string]interface{}{"key1": replacement3}, secretData(lastAppliedSecret)) }