Files
argo-cd/server/application/application_test.go
Alexander Matyushentsev f75984fbf5 Issue #1944 - Gracefully handle missing cached app state (#2464)
* Issue #1944 - Gracefully handle missing cached app state

* Unit test getCachedAppState method
2019-10-10 15:17:13 -07:00

507 lines
17 KiB
Go

package application
import (
"context"
coreerrors "errors"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
kubetesting "k8s.io/client-go/testing"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/pkg/apiclient/application"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
apps "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
appinformer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
mockrepo "github.com/argoproj/argo-cd/reposerver/mocks"
"github.com/argoproj/argo-cd/server/rbacpolicy"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/assets"
"github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/kube/kubetest"
"github.com/argoproj/argo-cd/util/rbac"
"github.com/argoproj/argo-cd/util/settings"
)
const (
testNamespace = "default"
fakeRepoURL = "https://git.com/repo.git"
)
type fakeCloser struct{}
func (f fakeCloser) Close() error {
return nil
}
func fakeRepo() *appsv1.Repository {
return &appsv1.Repository{
Repo: fakeRepoURL,
}
}
func fakeCluster() *appsv1.Cluster {
return &appsv1.Cluster{
Server: "https://cluster-api.com",
Name: "fake-cluster",
Config: appsv1.ClusterConfig{},
}
}
func fakeAppList() *apiclient.AppList {
return &apiclient.AppList{
Apps: map[string]string{
"some/path": "Ksonnet",
},
}
}
// return an ApplicationServiceServer which returns fake data
func newTestAppServer(objects ...runtime.Object) *Server {
kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "argocd-cm",
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
}, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: testNamespace,
},
Data: map[string][]byte{
"admin.password": []byte("test"),
"server.secretkey": []byte("test"),
},
})
db := db.NewDB(testNamespace, settings.NewSettingsManager(context.Background(), kubeclientset, testNamespace), kubeclientset)
ctx := context.Background()
_, err := db.CreateRepository(ctx, fakeRepo())
errors.CheckError(err)
_, err = db.CreateCluster(ctx, fakeCluster())
errors.CheckError(err)
mockRepoServiceClient := mocks.RepoServerServiceClient{}
mockRepoServiceClient.On("ListApps", mock.Anything, mock.Anything).Return(fakeAppList(), nil)
mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil)
mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil)
mockRepoClient := &mockrepo.Clientset{}
mockRepoClient.On("NewRepoServerClient").Return(&fakeCloser{}, &mockRepoServiceClient, nil)
defaultProj := &appsv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
Spec: appsv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
},
}
myProj := &appsv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"},
Spec: appsv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
},
}
projWithSyncWindows := &appsv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"},
Spec: appsv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
SyncWindows: appsv1.SyncWindows{},
},
}
matchingWindow := &appsv1.SyncWindow{
Kind: "allow",
Schedule: "* * * * *",
Duration: "1h",
Applications: []string{"test-app"},
}
projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow)
objects = append(objects, defaultProj, myProj, projWithSyncWindows)
fakeAppsClientset := apps.NewSimpleClientset(objects...)
factory := appinformer.NewFilteredSharedInformerFactory(fakeAppsClientset, 0, "", func(options *metav1.ListOptions) {})
fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace)
enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
_ = enforcer.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
enforcer.SetDefaultRole("role:admin")
enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims)
settingsMgr := settings.NewSettingsManager(context.Background(), kubeclientset, testNamespace)
server := NewServer(
testNamespace,
kubeclientset,
fakeAppsClientset,
mockRepoClient,
nil,
&kubetest.MockKubectlCmd{},
db,
enforcer,
util.NewKeyLock(),
settingsMgr,
)
return server.(*Server)
}
const fakeApp = `
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: test-app
namespace: default
spec:
source:
path: some/path
repoURL: https://github.com/argoproj/argocd-example-apps.git
targetRevision: HEAD
ksonnet:
environment: default
destination:
namespace: ` + test.FakeDestNamespace + `
server: https://cluster-api.com
`
func newTestApp() *appsv1.Application {
var app appsv1.Application
err := yaml.Unmarshal([]byte(fakeApp), &app)
if err != nil {
panic(err)
}
return &app
}
func TestCreateApp(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer()
testApp.Spec.Project = ""
createReq := application.ApplicationCreateRequest{
Application: *testApp,
}
app, err := appServer.Create(context.Background(), &createReq)
assert.NoError(t, err)
assert.NotNil(t, app)
assert.NotNil(t, app.Spec)
assert.Equal(t, app.Spec.Project, "default")
}
func TestUpdateApp(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer(testApp)
testApp.Spec.Project = ""
app, err := appServer.Update(context.Background(), &application.ApplicationUpdateRequest{
Application: testApp,
})
assert.Nil(t, err)
assert.Equal(t, app.Spec.Project, "default")
}
func TestUpdateAppSpec(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer(testApp)
testApp.Spec.Project = ""
spec, err := appServer.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{
Name: &testApp.Name,
Spec: testApp.Spec,
})
assert.NoError(t, err)
assert.Equal(t, "default", spec.Project)
app, err := appServer.Get(context.Background(), &application.ApplicationQuery{Name: &testApp.Name})
assert.NoError(t, err)
assert.Equal(t, "default", app.Spec.Project)
}
func TestDeleteApp(t *testing.T) {
ctx := context.Background()
appServer := newTestAppServer()
createReq := application.ApplicationCreateRequest{
Application: *newTestApp(),
}
app, err := appServer.Create(ctx, &createReq)
assert.Nil(t, err)
app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name})
assert.Nil(t, err)
assert.NotNil(t, app)
fakeAppCs := appServer.appclientset.(*apps.Clientset)
// this removes the default */* reactor so we can set our own patch/delete reactor
fakeAppCs.ReactionChain = nil
patched := false
deleted := false
fakeAppCs.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
patched = true
return true, nil, nil
})
fakeAppCs.AddReactor("delete", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
deleted = true
return true, nil, nil
})
appServer.appclientset = fakeAppCs
trueVar := true
_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar})
assert.Nil(t, err)
assert.True(t, patched)
assert.True(t, deleted)
// now call delete with cascade=false. patch should not be called
falseVar := false
patched = false
deleted = false
_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar})
assert.Nil(t, err)
assert.False(t, patched)
assert.True(t, deleted)
}
func TestSyncAndTerminate(t *testing.T) {
ctx := context.Background()
appServer := newTestAppServer()
testApp := newTestApp()
testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git"
createReq := application.ApplicationCreateRequest{
Application: *testApp,
}
app, err := appServer.Create(ctx, &createReq)
assert.Nil(t, err)
app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name})
assert.Nil(t, err)
assert.NotNil(t, app)
assert.NotNil(t, app.Operation)
events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(metav1.ListOptions{})
assert.Nil(t, err)
event := events.Items[1]
assert.Regexp(t, ".*initiated sync to HEAD \\([0-9A-Fa-f]{40}\\).*", event.Message)
// set status.operationState to pretend that an operation has started by controller
app.Status.OperationState = &appsv1.OperationState{
Operation: *app.Operation,
Phase: appsv1.OperationRunning,
StartedAt: metav1.NewTime(time.Now()),
}
_, err = appServer.appclientset.ArgoprojV1alpha1().Applications(appServer.ns).Update(app)
assert.Nil(t, err)
resp, err := appServer.TerminateOperation(ctx, &application.OperationTerminateRequest{Name: &app.Name})
assert.Nil(t, err)
assert.NotNil(t, resp)
app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name})
assert.Nil(t, err)
assert.NotNil(t, app)
assert.Equal(t, appsv1.OperationTerminating, app.Status.OperationState.Phase)
}
func TestRollbackApp(t *testing.T) {
testApp := newTestApp()
testApp.Status.History = []appsv1.RevisionHistory{{
ID: 1,
Revision: "abc",
Source: *testApp.Spec.Source.DeepCopy(),
}}
appServer := newTestAppServer(testApp)
updatedApp, err := appServer.Rollback(context.Background(), &application.ApplicationRollbackRequest{
Name: &testApp.Name,
ID: 1,
})
assert.Nil(t, err)
assert.NotNil(t, updatedApp.Operation)
assert.NotNil(t, updatedApp.Operation.Sync)
assert.NotNil(t, updatedApp.Operation.Sync.Source)
assert.Equal(t, "abc", updatedApp.Operation.Sync.Revision)
}
func TestUpdateAppProject(t *testing.T) {
testApp := newTestApp()
ctx := context.Background()
ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
appServer := newTestAppServer(testApp)
appServer.enf.SetDefaultRole("")
// Verify normal update works (without changing project)
_ = appServer.enf.SetBuiltinPolicy(`p, admin, applications, update, default/test-app, allow`)
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.NoError(t, err)
// Verify caller cannot update to another project
testApp.Spec.Project = "my-proj"
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
// Verify inability to change projects without create privileges in new project
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, update, default/test-app, allow
p, admin, applications, update, my-proj/test-app, allow
`)
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
// Verify inability to change projects without update privileges in new project
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, update, default/test-app, allow
p, admin, applications, create, my-proj/test-app, allow
`)
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
// Verify inability to change projects without update privileges in old project
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, create, my-proj/test-app, allow
p, admin, applications, update, my-proj/test-app, allow
`)
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
// Verify can update project with proper permissions
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, update, default/test-app, allow
p, admin, applications, create, my-proj/test-app, allow
p, admin, applications, update, my-proj/test-app, allow
`)
updatedApp, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.NoError(t, err)
assert.Equal(t, "my-proj", updatedApp.Spec.Project)
}
func TestAppJsonPatch(t *testing.T) {
testApp := newTestApp()
ctx := context.Background()
ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
appServer := newTestAppServer(testApp)
appServer.enf.SetDefaultRole("")
app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: "garbage"})
assert.Error(t, err)
assert.Nil(t, app)
app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: "[]"})
assert.NoError(t, err)
assert.NotNil(t, app)
app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: `[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`})
assert.NoError(t, err)
assert.Equal(t, "foo", app.Spec.Source.Path)
}
func TestAppMergePatch(t *testing.T) {
testApp := newTestApp()
ctx := context.Background()
ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
appServer := newTestAppServer(testApp)
appServer.enf.SetDefaultRole("")
app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{
Name: &testApp.Name, Patch: `{"spec": { "source": { "path": "foo" } }}`, PatchType: "merge"})
assert.NoError(t, err)
assert.Equal(t, "foo", app.Spec.Source.Path)
}
func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
t.Run("Active", func(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "proj-maint"
appServer := newTestAppServer(testApp)
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
assert.NoError(t, err)
assert.Equal(t, 1, len(active.ActiveWindows))
})
t.Run("Inactive", func(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "default"
appServer := newTestAppServer(testApp)
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
assert.NoError(t, err)
assert.Equal(t, 0, len(active.ActiveWindows))
})
t.Run("ProjectDoesNotExist", func(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "none"
appServer := newTestAppServer(testApp)
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
assert.Contains(t, err.Error(), "not found")
assert.Nil(t, active)
})
}
func TestGetCachedAppState(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "none"
appServer := newTestAppServer(testApp)
fakeClientSet := appServer.appclientset.(*apps.Clientset)
t.Run("NoError", func(t *testing.T) {
err := appServer.getCachedAppState(context.Background(), testApp, func() error {
return nil
})
assert.NoError(t, err)
})
t.Run("CacheMissErrorTriggersRefresh", func(t *testing.T) {
retryCount := 0
patched := false
watcher := watch.NewFakeWithChanSize(1, true)
fakeClientSet.ReactionChain = nil
fakeClientSet.WatchReactionChain = nil
fakeClientSet.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
patched = true
watcher.Modify(testApp)
return true, nil, nil
})
fakeClientSet.AddWatchReactor("applications", func(action kubetesting.Action) (handled bool, ret watch.Interface, err error) {
return true, watcher, nil
})
err := appServer.getCachedAppState(context.Background(), testApp, func() error {
res := cache.ErrCacheMiss
if retryCount == 1 {
res = nil
}
retryCount++
return res
})
assert.Equal(t, nil, err)
assert.Equal(t, 2, retryCount)
assert.True(t, patched)
})
t.Run("NonCacheErrorDoesNotTriggerRefresh", func(t *testing.T) {
randomError := coreerrors.New("random error")
err := appServer.getCachedAppState(context.Background(), testApp, func() error {
return randomError
})
assert.Equal(t, randomError, err)
})
}