mirror of
https://github.com/argoproj/argo-cd
synced 2026-05-24 09:50:08 +00:00
* feat: support background propagation policy Currently, Argo CD only supports foreground propagation policy ie. delete all the resources in the foreground and then delete the application in the end. This PR introduces a new flag `propagation-policy` to decide the type of policy when cascading is enabled. It also adds an annotation `propagation-policy.argocd.argoproj.io`, which is used by the application controller to decide the order of deletion. Fixes: #5216 Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * fix lint and doc errors Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * update logs to display the application name Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * address review comments * remove application name from logs since it's already present * update the propagation policy annotation key Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * Add radio buttons in the UI to select propagation policy Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * inject finalizers instead of annotations for specifying propagation policy Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * rebase branch on master Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * update the controller to set the policy only for application's resources Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com> * fix the label of policy radio button Signed-off-by: Chetan Banavikalmutt <chetanrns1997@gmail.com>
790 lines
26 KiB
Go
790 lines
26 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
coreerrors "errors"
|
|
"testing"
|
|
"time"
|
|
|
|
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
|
|
"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
|
|
"github.com/argoproj/pkg/sync"
|
|
"github.com/dgrijalva/jwt-go/v4"
|
|
"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"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
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"
|
|
k8scache "k8s.io/client-go/tools/cache"
|
|
"k8s.io/utils/pointer"
|
|
|
|
"github.com/argoproj/argo-cd/common"
|
|
"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"
|
|
"github.com/argoproj/argo-cd/server/rbacpolicy"
|
|
"github.com/argoproj/argo-cd/test"
|
|
"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/errors"
|
|
"github.com/argoproj/argo-cd/util/rbac"
|
|
"github.com/argoproj/argo-cd/util/settings"
|
|
)
|
|
|
|
const (
|
|
testNamespace = "default"
|
|
fakeRepoURL = "https://git.com/repo.git"
|
|
)
|
|
|
|
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"),
|
|
},
|
|
})
|
|
ctx := context.Background()
|
|
db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset)
|
|
_, 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 := &mocks.Clientset{RepoServerServiceClient: &mockRepoServiceClient}
|
|
|
|
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(ctx, kubeclientset, testNamespace)
|
|
|
|
// populate the app informer with the fake objects
|
|
appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
|
|
// TODO(jessesuen): probably should return cancel function so tests can stop background informer
|
|
//ctx, cancel := context.WithCancel(context.Background())
|
|
go appInformer.Run(ctx.Done())
|
|
if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
|
|
panic("Timed out waiting for caches to sync")
|
|
}
|
|
|
|
projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
|
|
go projInformer.Run(ctx.Done())
|
|
if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) {
|
|
panic("Timed out waiting for caches to sync")
|
|
}
|
|
|
|
server := NewServer(
|
|
testNamespace,
|
|
kubeclientset,
|
|
fakeAppsClientset,
|
|
factory.Argoproj().V1alpha1().Applications().Lister().Applications(testNamespace),
|
|
appInformer,
|
|
mockRepoClient,
|
|
nil,
|
|
&kubetest.MockKubectlCmd{},
|
|
db,
|
|
enforcer,
|
|
sync.NewKeyLock(),
|
|
settingsMgr,
|
|
projInformer,
|
|
)
|
|
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
|
|
`
|
|
|
|
const fakeAppWithDestName = `
|
|
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 + `
|
|
name: fake-cluster
|
|
`
|
|
|
|
const fakeAppWithAnnotations = `
|
|
apiVersion: argoproj.io/v1alpha1
|
|
kind: Application
|
|
metadata:
|
|
name: test-app
|
|
namespace: default
|
|
annotations:
|
|
test.annotation: test
|
|
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 newTestAppWithDestName(opts ...func(app *appsv1.Application)) *appsv1.Application {
|
|
return createTestApp(fakeAppWithDestName, opts...)
|
|
}
|
|
|
|
func newTestApp(opts ...func(app *appsv1.Application)) *appsv1.Application {
|
|
return createTestApp(fakeApp, opts...)
|
|
}
|
|
|
|
func newTestAppWithAnnotations(opts ...func(app *appsv1.Application)) *appsv1.Application {
|
|
return createTestApp(fakeAppWithAnnotations, opts...)
|
|
}
|
|
|
|
func createTestApp(testApp string, opts ...func(app *appsv1.Application)) *appsv1.Application {
|
|
var app appsv1.Application
|
|
err := yaml.Unmarshal([]byte(testApp), &app)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
for i := range opts {
|
|
opts[i](&app)
|
|
}
|
|
return &app
|
|
}
|
|
|
|
func TestListApps(t *testing.T) {
|
|
appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
|
|
app.Name = "bcd"
|
|
}), newTestApp(func(app *appsv1.Application) {
|
|
app.Name = "abc"
|
|
}), newTestApp(func(app *appsv1.Application) {
|
|
app.Name = "def"
|
|
}))
|
|
|
|
res, err := appServer.List(context.Background(), &application.ApplicationQuery{})
|
|
assert.NoError(t, err)
|
|
var names []string
|
|
for i := range res.Items {
|
|
names = append(names, res.Items[i].Name)
|
|
}
|
|
assert.Equal(t, []string{"abc", "bcd", "def"}, names)
|
|
}
|
|
|
|
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 TestCreateAppWithDestName(t *testing.T) {
|
|
appServer := newTestAppServer()
|
|
testApp := newTestAppWithDestName()
|
|
createReq := application.ApplicationCreateRequest{
|
|
Application: *testApp,
|
|
}
|
|
app, err := appServer.Create(context.Background(), &createReq)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, app)
|
|
assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.com")
|
|
}
|
|
|
|
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)
|
|
|
|
patched = false
|
|
deleted = false
|
|
revertValues := func() {
|
|
patched = false
|
|
deleted = false
|
|
}
|
|
|
|
t.Run("Delete with background propagation policy", func(t *testing.T) {
|
|
policy := backgroundPropagationPolicy
|
|
_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, PropagationPolicy: &policy})
|
|
assert.Nil(t, err)
|
|
assert.True(t, patched)
|
|
assert.True(t, deleted)
|
|
t.Cleanup(revertValues)
|
|
})
|
|
|
|
t.Run("Delete with cascade disabled and background propagation policy", func(t *testing.T) {
|
|
policy := backgroundPropagationPolicy
|
|
_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar, PropagationPolicy: &policy})
|
|
assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = cannot set propagation policy when cascading is disabled")
|
|
assert.False(t, patched)
|
|
assert.False(t, deleted)
|
|
t.Cleanup(revertValues)
|
|
})
|
|
|
|
t.Run("Delete with invalid propagation policy", func(t *testing.T) {
|
|
invalidPolicy := "invalid"
|
|
_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar, PropagationPolicy: &invalidPolicy})
|
|
assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = invalid propagation policy: invalid")
|
|
assert.False(t, patched)
|
|
assert.False(t, deleted)
|
|
t.Cleanup(revertValues)
|
|
})
|
|
|
|
t.Run("Delete with foreground propagation policy", func(t *testing.T) {
|
|
policy := foregroundPropagationPolicy
|
|
_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar, PropagationPolicy: &policy})
|
|
assert.Nil(t, err)
|
|
assert.True(t, patched)
|
|
assert.True(t, deleted)
|
|
t.Cleanup(revertValues)
|
|
})
|
|
}
|
|
|
|
func TestDeleteApp_InvalidName(t *testing.T) {
|
|
appServer := newTestAppServer()
|
|
_, err := appServer.Delete(context.Background(), &application.ApplicationDeleteRequest{
|
|
Name: pointer.StringPtr("foo"),
|
|
})
|
|
if !assert.Error(t, err) {
|
|
return
|
|
}
|
|
assert.True(t, apierrors.IsNotFound(err))
|
|
}
|
|
|
|
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(context.Background(), 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: synccommon.OperationRunning,
|
|
StartedAt: metav1.NewTime(time.Now()),
|
|
}
|
|
_, err = appServer.appclientset.ArgoprojV1alpha1().Applications(appServer.ns).Update(context.Background(), app, metav1.UpdateOptions{})
|
|
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, synccommon.OperationTerminating, app.Status.OperationState.Phase)
|
|
}
|
|
|
|
func TestSyncHelm(t *testing.T) {
|
|
ctx := context.Background()
|
|
appServer := newTestAppServer()
|
|
testApp := newTestApp()
|
|
testApp.Spec.Source.RepoURL = "https://argoproj.github.io/argo-helm"
|
|
testApp.Spec.Source.Path = ""
|
|
testApp.Spec.Source.Chart = "argo-cd"
|
|
testApp.Spec.Source.TargetRevision = "0.7.*"
|
|
|
|
app, err := appServer.Create(ctx, &application.ApplicationCreateRequest{Application: *testApp})
|
|
assert.NoError(t, err)
|
|
|
|
app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name})
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, app)
|
|
assert.NotNil(t, app.Operation)
|
|
|
|
events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(context.Background(), metav1.ListOptions{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Unknown user initiated sync to 0.7.* (0.7.2)", events.Items[1].Message)
|
|
}
|
|
|
|
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()
|
|
// nolint:staticcheck
|
|
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 := newTestAppWithAnnotations()
|
|
ctx := context.Background()
|
|
// nolint:staticcheck
|
|
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)
|
|
|
|
app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: `[{"op": "remove", "path": "/metadata/annotations/test.annotation"}]`})
|
|
assert.NoError(t, err)
|
|
assert.NotContains(t, app.Annotations, "test.annotation")
|
|
}
|
|
|
|
func TestAppMergePatch(t *testing.T) {
|
|
testApp := newTestApp()
|
|
ctx := context.Background()
|
|
// nolint:staticcheck
|
|
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.ObjectMeta.ResourceVersion = "1"
|
|
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)
|
|
|
|
// Configure fakeClientSet within lock, before requesting cached app state, to avoid data race
|
|
{
|
|
fakeClientSet.Lock()
|
|
fakeClientSet.ReactionChain = nil
|
|
fakeClientSet.WatchReactionChain = nil
|
|
fakeClientSet.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
|
patched = true
|
|
updated := testApp.DeepCopy()
|
|
updated.ResourceVersion = "2"
|
|
appServer.appBroadcaster.OnUpdate(testApp, updated)
|
|
return true, testApp, nil
|
|
})
|
|
fakeClientSet.AddWatchReactor("applications", func(action kubetesting.Action) (handled bool, ret watch.Interface, err error) {
|
|
return true, watcher, nil
|
|
})
|
|
fakeClientSet.Unlock()
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
func TestSplitStatusPatch(t *testing.T) {
|
|
specPatch := `{"spec":{"aaa":"bbb"}}`
|
|
statusPatch := `{"status":{"ccc":"ddd"}}`
|
|
{
|
|
nonStatus, status, err := splitStatusPatch([]byte(specPatch))
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, specPatch, string(nonStatus))
|
|
assert.Nil(t, status)
|
|
}
|
|
{
|
|
nonStatus, status, err := splitStatusPatch([]byte(statusPatch))
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, nonStatus)
|
|
assert.Equal(t, statusPatch, string(status))
|
|
}
|
|
{
|
|
bothPatch := `{"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}`
|
|
nonStatus, status, err := splitStatusPatch([]byte(bothPatch))
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, specPatch, string(nonStatus))
|
|
assert.Equal(t, statusPatch, string(status))
|
|
}
|
|
{
|
|
otherFields := `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}`
|
|
nonStatus, status, err := splitStatusPatch([]byte(otherFields))
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"}}`, string(nonStatus))
|
|
assert.Equal(t, statusPatch, string(status))
|
|
}
|
|
}
|
|
|
|
func TestLogsGetSelectedPod(t *testing.T) {
|
|
deployment := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "Deployment", Name: "deployment", UID: "1"}
|
|
rs := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "ReplicaSet", Name: "rs", UID: "2"}
|
|
podRS := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Name: "podrs", UID: "3"}
|
|
pod := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Name: "pod", UID: "4"}
|
|
treeNodes := []appsv1.ResourceNode{
|
|
{ResourceRef: deployment, ParentRefs: nil},
|
|
{ResourceRef: rs, ParentRefs: []appsv1.ResourceRef{deployment}},
|
|
{ResourceRef: podRS, ParentRefs: []appsv1.ResourceRef{rs}},
|
|
{ResourceRef: pod, ParentRefs: nil},
|
|
}
|
|
appName := "appName"
|
|
|
|
t.Run("GetAllPods", func(t *testing.T) {
|
|
podQuery := application.ApplicationPodLogsQuery{
|
|
Name: &appName,
|
|
}
|
|
pods := getSelectedPods(treeNodes, &podQuery)
|
|
assert.Equal(t, 2, len(pods))
|
|
})
|
|
|
|
t.Run("GetRSPods", func(t *testing.T) {
|
|
group := ""
|
|
kind := "ReplicaSet"
|
|
name := "rs"
|
|
podQuery := application.ApplicationPodLogsQuery{
|
|
Name: &appName,
|
|
Group: &group,
|
|
Kind: &kind,
|
|
ResourceName: &name,
|
|
}
|
|
pods := getSelectedPods(treeNodes, &podQuery)
|
|
assert.Equal(t, 1, len(pods))
|
|
})
|
|
|
|
t.Run("GetDeploymentPods", func(t *testing.T) {
|
|
group := ""
|
|
kind := "Deployment"
|
|
name := "deployment"
|
|
podQuery := application.ApplicationPodLogsQuery{
|
|
Name: &appName,
|
|
Group: &group,
|
|
Kind: &kind,
|
|
ResourceName: &name,
|
|
}
|
|
pods := getSelectedPods(treeNodes, &podQuery)
|
|
assert.Equal(t, 1, len(pods))
|
|
})
|
|
|
|
t.Run("NoMatchingPods", func(t *testing.T) {
|
|
group := ""
|
|
kind := "Service"
|
|
name := "service"
|
|
podQuery := application.ApplicationPodLogsQuery{
|
|
Name: &appName,
|
|
Group: &group,
|
|
Kind: &kind,
|
|
ResourceName: &name,
|
|
}
|
|
pods := getSelectedPods(treeNodes, &podQuery)
|
|
assert.Equal(t, 0, len(pods))
|
|
})
|
|
}
|