argo-cd/util/argo/argo_test.go
Blake Pettersson 18c4d9d568
feat: oci support (Beta) (#18646)
Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2025-06-06 13:27:02 +02:00

1712 lines
57 KiB
Go

package argo
import (
"context"
"errors"
"fmt"
"path/filepath"
"testing"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
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/schema"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
"github.com/argoproj/gitops-engine/pkg/sync/common"
argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks"
"github.com/argoproj/argo-cd/v3/test"
"github.com/argoproj/argo-cd/v3/util/db"
dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks"
"github.com/argoproj/argo-cd/v3/util/settings"
)
func TestRefreshApp(t *testing.T) {
var testApp argoappv1.Application
testApp.Name = "test-app"
testApp.Namespace = "default"
appClientset := appclientset.NewSimpleClientset(&testApp)
appIf := appClientset.ArgoprojV1alpha1().Applications("default")
_, err := RefreshApp(appIf, "test-app", argoappv1.RefreshTypeNormal, true)
require.NoError(t, err)
// For some reason, the fake Application interface doesn't reflect the patch status after Patch(),
// so can't verify it was set in unit tests.
// _, ok := newApp.Annotations[common.AnnotationKeyRefresh]
// assert.True(t, ok)
}
func TestGetAppProjectWithNoProjDefined(t *testing.T) {
projName := "default"
namespace := "default"
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: test.FakeArgoCDNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
}
testProj := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: projName, Namespace: namespace},
}
var testApp argoappv1.Application
testApp.Name = "test-app"
testApp.Namespace = namespace
appClientset := appclientset.NewSimpleClientset(testProj)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
informer := v1alpha1.NewAppProjectInformer(appClientset, namespace, 0, indexers)
go informer.Run(ctx.Done())
cache.WaitForCacheSync(ctx.Done(), informer.HasSynced)
kubeClient := fake.NewClientset(&cm)
settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace)
argoDB := db.NewDB("default", settingsMgr, kubeClient)
proj, err := GetAppProject(ctx, &testApp, applisters.NewAppProjectLister(informer.GetIndexer()), namespace, settingsMgr, argoDB)
require.NoError(t, err)
assert.Equal(t, proj.Name, projName)
}
func TestIncludeResource(t *testing.T) {
// Resource filters format - GROUP:KIND:NAMESPACE/NAME or GROUP:KIND:NAME
var (
blankValues = argoappv1.SyncOperationResource{Group: "", Kind: "", Name: "", Namespace: "", Exclude: false}
// *:*:*
includeAllResources = argoappv1.SyncOperationResource{Group: "*", Kind: "*", Name: "*", Namespace: "", Exclude: false}
// !*:*:*
excludeAllResources = argoappv1.SyncOperationResource{Group: "*", Kind: "*", Name: "*", Namespace: "", Exclude: true}
// *:Service:*
includeAllServiceResources = argoappv1.SyncOperationResource{Group: "*", Kind: "Service", Name: "*", Namespace: "", Exclude: false}
// !*:Service:*
excludeAllServiceResources = argoappv1.SyncOperationResource{Group: "*", Kind: "Service", Name: "*", Namespace: "", Exclude: true}
// apps:ReplicaSet:backend
includeAllReplicaSetResource = argoappv1.SyncOperationResource{Group: "apps", Kind: "ReplicaSet", Name: "*", Namespace: "", Exclude: false}
// apps:ReplicaSet:backend
includeReplicaSetResource = argoappv1.SyncOperationResource{Group: "apps", Kind: "ReplicaSet", Name: "backend", Namespace: "", Exclude: false}
// !apps:ReplicaSet:backend
excludeReplicaSetResource = argoappv1.SyncOperationResource{Group: "apps", Kind: "ReplicaSet", Name: "backend", Namespace: "", Exclude: true}
)
tests := []struct {
testName string
name string
namespace string
gvk schema.GroupVersionKind
syncOperationResource []*argoappv1.SyncOperationResource
expectedResult bool
}{
//--resource apps:ReplicaSet:backend --resource *:Service:*
{
testName: "Include ReplicaSet backend resource and all service resources",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"},
syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllServiceResources, &includeReplicaSetResource},
expectedResult: true,
},
//--resource apps:ReplicaSet:backend --resource *:Service:*
{
testName: "Include ReplicaSet backend resource and all service resources",
name: "main-page-down",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "batch", Kind: "Job"},
syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllServiceResources, &includeReplicaSetResource},
expectedResult: false,
},
//--resource apps:ReplicaSet:backend --resource !*:Service:*
{
testName: "Include ReplicaSet backend resource and exclude all service resources",
name: "main-page-down",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "batch", Kind: "Job"},
syncOperationResource: []*argoappv1.SyncOperationResource{&excludeAllServiceResources, &includeReplicaSetResource},
expectedResult: false,
},
// --resource !apps:ReplicaSet:backend --resource !*:Service:*
{
testName: "Exclude ReplicaSet backend resource and all service resources",
name: "main-page-down",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "batch", Kind: "Job"},
syncOperationResource: []*argoappv1.SyncOperationResource{&excludeReplicaSetResource, &excludeAllServiceResources},
expectedResult: true,
},
// --resource !apps:ReplicaSet:backend
{
testName: "Exclude ReplicaSet backend resource",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"},
syncOperationResource: []*argoappv1.SyncOperationResource{&excludeReplicaSetResource},
expectedResult: false,
},
// --resource !apps:ReplicaSet:backend --resource !*:Service:*
{
testName: "Exclude ReplicaSet backend resource and all service resources(dummy condition)",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"},
syncOperationResource: []*argoappv1.SyncOperationResource{&excludeReplicaSetResource, &excludeAllServiceResources},
expectedResult: false,
},
// --resource apps:ReplicaSet:backend
{
testName: "Include ReplicaSet backend resource",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"},
syncOperationResource: []*argoappv1.SyncOperationResource{&includeReplicaSetResource},
expectedResult: true,
},
// --resource !*:Service:*
{
testName: "Exclude Service resources",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "", Kind: "Service"},
syncOperationResource: []*argoappv1.SyncOperationResource{&excludeAllServiceResources},
expectedResult: false,
},
// --resource *:Service:*
{
testName: "Include Service resources",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "", Kind: "Service"},
syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllServiceResources},
expectedResult: true,
},
// --resource apps:ReplicaSet:* --resource !apps:ReplicaSet:backend
{
testName: "Include & Exclude ReplicaSet resources",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"},
syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllReplicaSetResource, &excludeReplicaSetResource},
expectedResult: false,
},
// --resource !*:*:*
{
testName: "Exclude all resources",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "", Kind: "Service"},
syncOperationResource: []*argoappv1.SyncOperationResource{&excludeAllResources},
expectedResult: false,
},
// --resource *:*:*
{
testName: "Include all resources",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "", Kind: "Service"},
syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllResources},
expectedResult: true,
},
{
testName: "No Filters",
name: "backend",
namespace: "default",
gvk: schema.GroupVersionKind{Group: "", Kind: "Service"},
syncOperationResource: []*argoappv1.SyncOperationResource{&blankValues},
expectedResult: false,
},
{
testName: "Default values",
expectedResult: true,
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
isResourceIncluded := IncludeResource(test.name, test.namespace, test.gvk, test.syncOperationResource)
assert.Equal(t, test.expectedResult, isResourceIncluded)
})
}
}
func TestContainsSyncResource(t *testing.T) {
var (
blankUnstructured unstructured.Unstructured
blankResource argoappv1.SyncOperationResource
helloResource = argoappv1.SyncOperationResource{Name: "hello"}
)
tables := []struct {
u *unstructured.Unstructured
rr []argoappv1.SyncOperationResource
expected bool
}{
{&blankUnstructured, []argoappv1.SyncOperationResource{}, false},
{&blankUnstructured, []argoappv1.SyncOperationResource{blankResource}, true},
{&blankUnstructured, []argoappv1.SyncOperationResource{helloResource}, false},
}
for _, table := range tables {
out := ContainsSyncResource(table.u.GetName(), table.u.GetNamespace(), table.u.GroupVersionKind(), table.rr)
assert.Equal(t, table.expected, out, "Expected %t for slice %+v contains resource %+v; instead got %t", table.expected, table.rr, table.u, out)
}
}
// TestNilOutZerValueAppSources verifies we will nil out app source specs when they are their zero-value
func TestNilOutZerValueAppSources(t *testing.T) {
var spec *argoappv1.ApplicationSpec
{
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NamePrefix: "foo"}}})
assert.NotNil(t, spec.GetSource().Kustomize)
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NamePrefix: ""}}})
source := spec.GetSource()
assert.Nil(t, source.Kustomize)
}
{
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NameSuffix: "foo"}}})
assert.NotNil(t, spec.GetSource().Kustomize)
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NameSuffix: ""}}})
source := spec.GetSource()
assert.Nil(t, source.Kustomize)
}
{
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Helm: &argoappv1.ApplicationSourceHelm{ValueFiles: []string{"values.yaml"}}}})
assert.NotNil(t, spec.GetSource().Helm)
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Helm: &argoappv1.ApplicationSourceHelm{ValueFiles: []string{}}}})
assert.Nil(t, spec.GetSource().Helm)
}
{
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}})
assert.NotNil(t, spec.GetSource().Directory)
spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Directory: &argoappv1.ApplicationSourceDirectory{Recurse: false}}})
assert.Nil(t, spec.GetSource().Directory)
}
}
func TestValidatePermissionsEmptyDestination(t *testing.T) {
conditions, err := ValidatePermissions(t.Context(), &argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{RepoURL: "https://github.com/argoproj/argo-cd", Path: "."},
}, &argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []argoappv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
},
}, nil)
require.NoError(t, err)
assert.ElementsMatch(t, conditions, []argoappv1.ApplicationCondition{{Type: argoappv1.ApplicationConditionInvalidSpecError, Message: "Destination server missing from app spec"}})
}
func TestValidateChartWithoutRevision(t *testing.T) {
appSpec := &argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{RepoURL: "https://charts.helm.sh/incubator/", Chart: "myChart", TargetRevision: ""},
Destination: argoappv1.ApplicationDestination{
Server: "https://kubernetes.default.svc", Namespace: "default",
},
}
cluster := &argoappv1.Cluster{Server: "https://kubernetes.default.svc"}
db := &dbmocks.ArgoDB{}
ctx := t.Context()
db.On("GetCluster", ctx, appSpec.Destination.Server).Return(cluster, nil)
conditions, err := ValidatePermissions(ctx, appSpec, &argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []argoappv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
},
}, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type)
assert.Equal(t, "spec.source.targetRevision is required if the manifest source is a helm chart", conditions[0].Message)
}
func TestAPIResourcesToStrings(t *testing.T) {
resources := []kube.APIResourceInfo{{
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta1"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}, {
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta2"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}, {
GroupVersionResource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}}
assert.ElementsMatch(t, []string{"apps/v1beta1", "apps/v1beta2", "extensions/v1beta1"}, APIResourcesToStrings(resources, false))
assert.ElementsMatch(t, []string{
"apps/v1beta1", "apps/v1beta1/Deployment", "apps/v1beta2", "apps/v1beta2/Deployment", "extensions/v1beta1", "extensions/v1beta1/Deployment",
},
APIResourcesToStrings(resources, true))
}
func TestValidateRepo(t *testing.T) {
repoPath, err := filepath.Abs("./../..")
require.NoError(t, err)
apiResources := []kube.APIResourceInfo{{
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta1"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}, {
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta2"},
GroupKind: schema.GroupKind{Kind: "Deployment"},
}}
kubeVersion := "v1.16"
kustomizeOptions := &argoappv1.KustomizeOptions{BuildOptions: ""}
repo := &argoappv1.Repository{Repo: "file://" + repoPath}
cluster := &argoappv1.Cluster{Server: "sample server"}
app := &argoappv1.Application{
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: repo.Repo,
},
Destination: argoappv1.ApplicationDestination{
Server: cluster.Server,
Namespace: "default",
},
},
}
proj := &argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
SourceRepos: []string{"*"},
},
}
helmRepos := []*argoappv1.Repository{{Repo: "sample helm repo"}}
repoClient := &mocks.RepoServerServiceClient{}
source := app.Spec.GetSource()
repoClient.On("GetAppDetails", t.Context(), &apiclient.RepoServerAppDetailsQuery{
Repo: repo,
Source: &source,
Repos: helmRepos,
KustomizeOptions: kustomizeOptions,
HelmOptions: &argoappv1.HelmOptions{ValuesFileSchemes: []string{"https", "http"}},
NoRevisionCache: true,
}).Return(&apiclient.RepoAppDetailsResponse{}, nil)
repo.Type = "git"
repoClient.On("TestRepository", t.Context(), &apiclient.TestRepositoryRequest{
Repo: repo,
}).Return(&apiclient.TestRepositoryResponse{
VerifiedRepository: true,
}, nil)
repoClientSet := &mocks.Clientset{RepoServerServiceClient: repoClient}
db := &dbmocks.ArgoDB{}
db.On("GetRepository", t.Context(), app.Spec.Source.RepoURL, "").Return(repo, nil)
db.On("ListHelmRepositories", t.Context()).Return(helmRepos, nil)
db.On("ListOCIRepositories", t.Context()).Return([]*argoappv1.Repository{}, nil)
db.On("GetCluster", t.Context(), app.Spec.Destination.Server).Return(cluster, nil)
db.On("GetAllHelmRepositoryCredentials", t.Context()).Return(nil, nil)
db.On("GetAllOCIRepositoryCredentials", t.Context()).Return([]*argoappv1.RepoCreds{}, nil)
var receivedRequest *apiclient.ManifestRequest
repoClient.On("GenerateManifest", t.Context(), mock.MatchedBy(func(req *apiclient.ManifestRequest) bool {
receivedRequest = req
return true
})).Return(nil, nil)
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: test.FakeArgoCDNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"globalProjects": `
- projectName: default-x
labelSelector:
matchExpressions:
- key: is-x
operator: Exists
- projectName: default-non-x
labelSelector:
matchExpressions:
- key: is-x
operator: DoesNotExist
`,
},
}
kubeClient := fake.NewClientset(&cm)
settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace)
conditions, err := ValidateRepo(t.Context(), app, repoClientSet, db, &kubetest.MockKubectlCmd{Version: kubeVersion, APIResources: apiResources}, proj, settingsMgr)
require.NoError(t, err)
assert.Empty(t, conditions)
assert.ElementsMatch(t, []string{"apps/v1beta1", "apps/v1beta1/Deployment", "apps/v1beta2", "apps/v1beta2/Deployment"}, receivedRequest.ApiVersions)
assert.Equal(t, kubeVersion, receivedRequest.KubeVersion)
assert.Equal(t, app.Spec.Destination.Namespace, receivedRequest.Namespace)
assert.Equal(t, &source, receivedRequest.ApplicationSource)
assert.Equal(t, kustomizeOptions, receivedRequest.KustomizeOptions)
}
func TestFormatAppConditions(t *testing.T) {
conditions := []argoappv1.ApplicationCondition{
{
Type: EventReasonOperationCompleted,
Message: "Foo",
},
{
Type: EventReasonResourceCreated,
Message: "Bar",
},
}
t.Run("Single Condition", func(t *testing.T) {
res := FormatAppConditions(conditions[0:1])
assert.NotEmpty(t, res)
assert.Equal(t, EventReasonOperationCompleted+": Foo", res)
})
t.Run("Multiple Conditions", func(t *testing.T) {
res := FormatAppConditions(conditions)
assert.NotEmpty(t, res)
assert.Equal(t, fmt.Sprintf("%s: Foo;%s: Bar", EventReasonOperationCompleted, EventReasonResourceCreated), res)
})
t.Run("Empty Conditions", func(t *testing.T) {
res := FormatAppConditions([]argoappv1.ApplicationCondition{})
assert.Empty(t, res)
})
}
func TestFilterByProjects(t *testing.T) {
apps := []argoappv1.Application{
{
Spec: argoappv1.ApplicationSpec{
Project: "fooproj",
},
},
{
Spec: argoappv1.ApplicationSpec{
Project: "barproj",
},
},
}
t.Run("No apps in single project", func(t *testing.T) {
res := FilterByProjects(apps, []string{"foobarproj"})
assert.Empty(t, res)
})
t.Run("Single app in single project", func(t *testing.T) {
res := FilterByProjects(apps, []string{"fooproj"})
assert.Len(t, res, 1)
})
t.Run("Single app in multiple project", func(t *testing.T) {
res := FilterByProjects(apps, []string{"fooproj", "foobarproj"})
assert.Len(t, res, 1)
})
t.Run("Multiple apps in multiple project", func(t *testing.T) {
res := FilterByProjects(apps, []string{"fooproj", "barproj"})
assert.Len(t, res, 2)
})
}
func TestFilterByProjectsP(t *testing.T) {
apps := []*argoappv1.Application{
{
Spec: argoappv1.ApplicationSpec{
Project: "fooproj",
},
},
{
Spec: argoappv1.ApplicationSpec{
Project: "barproj",
},
},
}
t.Run("No apps in single project", func(t *testing.T) {
res := FilterByProjectsP(apps, []string{"foobarproj"})
assert.Empty(t, res)
})
t.Run("Single app in single project", func(t *testing.T) {
res := FilterByProjectsP(apps, []string{"fooproj"})
assert.Len(t, res, 1)
})
t.Run("Single app in multiple project", func(t *testing.T) {
res := FilterByProjectsP(apps, []string{"fooproj", "foobarproj"})
assert.Len(t, res, 1)
})
t.Run("Multiple apps in multiple project", func(t *testing.T) {
res := FilterByProjectsP(apps, []string{"fooproj", "barproj"})
assert.Len(t, res, 2)
})
}
func TestFilterByRepo(t *testing.T) {
apps := []argoappv1.Application{
{
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "git@github.com:owner/repo.git",
},
},
},
{
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "git@github.com:owner/otherrepo.git",
},
},
},
}
t.Run("Empty filter", func(t *testing.T) {
res := FilterByRepo(apps, "")
assert.Len(t, res, 2)
})
t.Run("Match", func(t *testing.T) {
res := FilterByRepo(apps, "git@github.com:owner/repo.git")
assert.Len(t, res, 1)
})
t.Run("No match", func(t *testing.T) {
res := FilterByRepo(apps, "git@github.com:owner/willnotmatch.git")
assert.Empty(t, res)
})
}
func TestFilterByRepoP(t *testing.T) {
apps := []*argoappv1.Application{
{
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "git@github.com:owner/repo.git",
},
},
},
{
Spec: argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "git@github.com:owner/otherrepo.git",
},
},
},
}
t.Run("Empty filter", func(t *testing.T) {
res := FilterByRepoP(apps, "")
assert.Len(t, res, 2)
})
t.Run("Match", func(t *testing.T) {
res := FilterByRepoP(apps, "git@github.com:owner/repo.git")
assert.Len(t, res, 1)
})
t.Run("No match", func(t *testing.T) {
res := FilterByRepoP(apps, "git@github.com:owner/willnotmatch.git")
assert.Empty(t, res)
})
}
func TestValidatePermissions(t *testing.T) {
t.Run("Empty Repo URL result in condition", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "",
},
}
proj := argoappv1.AppProject{}
db := &dbmocks.ArgoDB{}
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type)
assert.Contains(t, conditions[0].Message, "are required")
})
t.Run("Incomplete Path/Chart combo result in condition", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "",
},
}
proj := argoappv1.AppProject{}
db := &dbmocks.ArgoDB{}
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type)
assert.Contains(t, conditions[0].Message, "are required")
})
t.Run("Helm chart requires targetRevision", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
},
}
proj := argoappv1.AppProject{}
db := &dbmocks.ArgoDB{}
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type)
assert.Contains(t, conditions[0].Message, "is required if the manifest source is a helm chart")
})
t.Run("Application source is not permitted in project", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
Destination: argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "testns",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "*",
},
},
SourceRepos: []string{"http://some/where/else"},
},
}
cluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "test"}
db := &dbmocks.ArgoDB{}
db.On("GetCluster", t.Context(), spec.Destination.Server).Return(cluster, nil)
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Contains(t, conditions[0].Message, "application repo http://some/where is not permitted")
})
t.Run("Application destination is not permitted in project", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
Destination: argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "testns",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "default",
},
},
SourceRepos: []string{"http://some/where"},
},
}
cluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "test"}
db := &dbmocks.ArgoDB{}
db.On("GetCluster", t.Context(), spec.Destination.Server).Return(cluster, nil)
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Contains(t, conditions[0].Message, "application destination")
})
t.Run("Destination cluster does not exist", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
Destination: argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "default",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "default",
},
},
SourceRepos: []string{"http://some/where"},
},
}
db := &dbmocks.ArgoDB{}
db.On("GetCluster", t.Context(), spec.Destination.Server).Return(nil, status.Errorf(codes.NotFound, "Cluster does not exist"))
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Contains(t, conditions[0].Message, "Cluster does not exist")
})
t.Run("Destination cluster name does not exist", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
Destination: argoappv1.ApplicationDestination{
Name: "does-not-exist",
Namespace: "default",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "default",
},
},
SourceRepos: []string{"http://some/where"},
},
}
db := &dbmocks.ArgoDB{}
db.On("GetClusterServersByName", t.Context(), "does-not-exist").Return(nil, nil)
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Contains(t, conditions[0].Message, "there are no clusters with this name: does-not-exist")
})
t.Run("Cannot get cluster info from DB", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
Destination: argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "default",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "default",
},
},
SourceRepos: []string{"http://some/where"},
},
}
db := &dbmocks.ArgoDB{}
db.On("GetCluster", t.Context(), spec.Destination.Server).Return(nil, errors.New("Unknown error occurred"))
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Contains(t, conditions[0].Message, "Unknown error occurred")
})
t.Run("Destination cluster name resolves to valid server", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Source: &argoappv1.ApplicationSource{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
Destination: argoappv1.ApplicationDestination{
Name: "does-exist",
Namespace: "default",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "default",
},
},
SourceRepos: []string{"http://some/where"},
},
}
db := &dbmocks.ArgoDB{}
cluster := argoappv1.Cluster{
Name: "does-exist",
Server: "https://127.0.0.1:6443",
}
db.On("GetClusterServersByName", t.Context(), "does-exist").Return([]string{"https://127.0.0.1:6443"}, nil)
db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(&cluster, nil)
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Empty(t, conditions)
})
}
func TestSetAppOperations(t *testing.T) {
t.Run("Application not existing", func(t *testing.T) {
appIf := appclientset.NewSimpleClientset().ArgoprojV1alpha1().Applications("default")
app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}})
require.Error(t, err)
assert.Nil(t, app)
})
t.Run("Operation already in progress", func(t *testing.T) {
a := argoappv1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "someapp",
Namespace: "default",
},
Operation: &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}},
}
appIf := appclientset.NewSimpleClientset(&a).ArgoprojV1alpha1().Applications("default")
app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}})
require.ErrorContains(t, err, "operation is already in progress")
assert.Nil(t, app)
})
t.Run("Operation unspecified", func(t *testing.T) {
a := argoappv1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "someapp",
Namespace: "default",
},
}
appIf := appclientset.NewSimpleClientset(&a).ArgoprojV1alpha1().Applications("default")
app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: nil})
require.ErrorContains(t, err, "Operation unspecified")
assert.Nil(t, app)
})
t.Run("Success", func(t *testing.T) {
a := argoappv1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "someapp",
Namespace: "default",
},
}
appIf := appclientset.NewSimpleClientset(&a).ArgoprojV1alpha1().Applications("default")
app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}})
require.NoError(t, err)
assert.NotNil(t, app)
})
}
func TestGetDestinationCluster(t *testing.T) {
t.Run("Validate destination with server url", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "default",
}
expectedCluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443"}
db := &dbmocks.ArgoDB{}
db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(expectedCluster, nil)
destCluster, err := GetDestinationCluster(t.Context(), dest, db)
require.NoError(t, err)
require.NotNil(t, expectedCluster)
assert.Equal(t, expectedCluster, destCluster)
})
t.Run("Validate destination with server name", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "minikube",
}
db := &dbmocks.ArgoDB{}
db.On("GetClusterServersByName", t.Context(), "minikube").Return([]string{"https://127.0.0.1:6443"}, nil)
db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(&argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "minikube"}, nil)
destCluster, err := GetDestinationCluster(t.Context(), dest, db)
require.NoError(t, err)
assert.Equal(t, "https://127.0.0.1:6443", destCluster.Server)
})
t.Run("Error when having both server url and name", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Name: "minikube",
Namespace: "default",
}
_, err := GetDestinationCluster(t.Context(), dest, nil)
assert.EqualError(t, err, "application destination can't have both name and server defined: minikube https://127.0.0.1:6443")
})
t.Run("GetClusterServersByName fails", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "minikube",
}
db := &dbmocks.ArgoDB{}
db.On("GetClusterServersByName", t.Context(), mock.Anything).Return(nil, errors.New("an error occurred"))
_, err := GetDestinationCluster(t.Context(), dest, db)
require.ErrorContains(t, err, "an error occurred")
})
t.Run("Destination cluster does not exist", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "minikube",
}
db := &dbmocks.ArgoDB{}
db.On("GetClusterServersByName", t.Context(), "minikube").Return(nil, nil)
_, err := GetDestinationCluster(t.Context(), dest, db)
assert.EqualError(t, err, "there are no clusters with this name: minikube")
})
t.Run("Validate too many clusters with the same name", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "dind",
}
db := &dbmocks.ArgoDB{}
db.On("GetClusterServersByName", t.Context(), "dind").Return([]string{"https://127.0.0.1:2443", "https://127.0.0.1:8443"}, nil)
_, err := GetDestinationCluster(t.Context(), dest, db)
assert.EqualError(t, err, "there are 2 clusters with the same name: [https://127.0.0.1:2443 https://127.0.0.1:8443]")
})
}
func TestFilterByName(t *testing.T) {
apps := []argoappv1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: argoappv1.ApplicationSpec{
Project: "fooproj",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
},
Spec: argoappv1.ApplicationSpec{
Project: "barproj",
},
},
}
t.Run("Name is empty string", func(t *testing.T) {
res, err := FilterByName(apps, "")
require.NoError(t, err)
assert.Len(t, res, 2)
})
t.Run("Single app by name", func(t *testing.T) {
res, err := FilterByName(apps, "foo")
require.NoError(t, err)
assert.Len(t, res, 1)
})
t.Run("No such app", func(t *testing.T) {
res, err := FilterByName(apps, "foobar")
require.Error(t, err)
assert.Empty(t, res)
})
}
func TestFilterByNameP(t *testing.T) {
apps := []*argoappv1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: argoappv1.ApplicationSpec{
Project: "fooproj",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
},
Spec: argoappv1.ApplicationSpec{
Project: "barproj",
},
},
}
t.Run("Name is empty string", func(t *testing.T) {
res := FilterByNameP(apps, "")
assert.Len(t, res, 2)
})
t.Run("Single app by name", func(t *testing.T) {
res := FilterByNameP(apps, "foo")
assert.Len(t, res, 1)
})
t.Run("No such app", func(t *testing.T) {
res := FilterByNameP(apps, "foobar")
assert.Empty(t, res)
})
}
func TestGetGlobalProjects(t *testing.T) {
t.Run("Multiple global projects", func(t *testing.T) {
namespace := "default"
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: test.FakeArgoCDNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"globalProjects": `
- projectName: default-x
labelSelector:
matchExpressions:
- key: is-x
operator: Exists
- projectName: default-non-x
labelSelector:
matchExpressions:
- key: is-x
operator: DoesNotExist
`,
},
}
defaultX := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default-x", Namespace: namespace},
Spec: argoappv1.AppProjectSpec{
ClusterResourceWhitelist: []metav1.GroupKind{
{Group: "*", Kind: "*"},
},
ClusterResourceBlacklist: []metav1.GroupKind{
{Kind: "Volume"},
},
},
}
defaultNonX := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default-non-x", Namespace: namespace},
Spec: argoappv1.AppProjectSpec{
ClusterResourceBlacklist: []metav1.GroupKind{
{Group: "*", Kind: "*"},
},
},
}
isX := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "is-x",
Namespace: namespace,
Labels: map[string]string{
"is-x": "yep",
},
},
}
isNoX := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "is-no-x", Namespace: namespace},
}
projClientset := appclientset.NewSimpleClientset(defaultX, defaultNonX, isX, isNoX)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
informer := v1alpha1.NewAppProjectInformer(projClientset, namespace, 0, indexers)
go informer.Run(ctx.Done())
cache.WaitForCacheSync(ctx.Done(), informer.HasSynced)
kubeClient := fake.NewSimpleClientset(&cm)
settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace)
projLister := applisters.NewAppProjectLister(informer.GetIndexer())
xGlobalProjects := GetGlobalProjects(isX, projLister, settingsMgr)
assert.Len(t, xGlobalProjects, 1)
assert.Equal(t, "default-x", xGlobalProjects[0].Name)
nonXGlobalProjects := GetGlobalProjects(isNoX, projLister, settingsMgr)
assert.Len(t, nonXGlobalProjects, 1)
assert.Equal(t, "default-non-x", nonXGlobalProjects[0].Name)
})
}
func Test_GetDifferentPathsBetweenStructs(t *testing.T) {
r1 := argoappv1.Repository{}
r2 := argoappv1.Repository{
Name: "SomeName",
}
difference, _ := GetDifferentPathsBetweenStructs(r1, r2)
assert.Equal(t, []string{"Name"}, difference)
}
func Test_GenerateSpecIsDifferentErrorMessageWithNoDiff(t *testing.T) {
r1 := argoappv1.Repository{}
r2 := argoappv1.Repository{}
msg := GenerateSpecIsDifferentErrorMessage("application", r1, r2)
assert.Equal(t, "existing application spec is different; use upsert flag to force update", msg)
}
func Test_GenerateSpecIsDifferentErrorMessageWithDiff(t *testing.T) {
r1 := argoappv1.Repository{}
r2 := argoappv1.Repository{
Name: "test",
}
msg := GenerateSpecIsDifferentErrorMessage("repo", r1, r2)
assert.Equal(t, "existing repo spec is different; use upsert flag to force update; difference in keys \"Name\"", msg)
}
func Test_ParseAppQualifiedName(t *testing.T) {
testcases := []struct {
name string
input string
implicitNs string
appName string
appNs string
}{
{"Full qualified without implicit NS", "namespace/name", "", "name", "namespace"},
{"Non qualified without implicit NS", "name", "", "name", ""},
{"Full qualified with implicit NS", "namespace/name", "namespace2", "name", "namespace"},
{"Non qualified with implicit NS", "name", "namespace2", "name", "namespace2"},
{"Invalid without implicit NS", "namespace_name", "", "namespace_name", ""},
}
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
appName, appNs := ParseFromQualifiedName(tt.input, tt.implicitNs)
assert.Equal(t, tt.appName, appName)
assert.Equal(t, tt.appNs, appNs)
})
}
}
func Test_ParseAppInstanceName(t *testing.T) {
testcases := []struct {
name string
input string
implicitNs string
appName string
appNs string
}{
{"Full qualified without implicit NS", "namespace_name", "", "name", "namespace"},
{"Non qualified without implicit NS", "name", "", "name", ""},
{"Full qualified with implicit NS", "namespace_name", "namespace2", "name", "namespace"},
{"Non qualified with implicit NS", "name", "namespace2", "name", "namespace2"},
{"Invalid without implicit NS", "namespace/name", "", "namespace/name", ""},
}
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
appName, appNs := ParseInstanceName(tt.input, tt.implicitNs)
assert.Equal(t, tt.appName, appName)
assert.Equal(t, tt.appNs, appNs)
})
}
}
func Test_AppInstanceName(t *testing.T) {
testcases := []struct {
name string
appName string
appNamespace string
defaultNs string
result string
}{
{"defaultns different as appns", "appname", "appns", "defaultns", "appns_appname"},
{"defaultns same as appns", "appname", "appns", "appns", "appname"},
{"defaultns set and appns not given", "appname", "", "appns", "appname"},
{"neither defaultns nor appns set", "appname", "", "appns", "appname"},
}
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
result := AppInstanceName(tt.appName, tt.appNamespace, tt.defaultNs)
assert.Equal(t, tt.result, result)
})
}
}
func Test_AppInstanceNameFromQualified(t *testing.T) {
testcases := []struct {
name string
appName string
defaultNs string
result string
}{
{"Qualified name with namespace not being defaultns", "appns/appname", "defaultns", "appns_appname"},
{"Qualified name with namespace being defaultns", "defaultns/appname", "defaultns", "appname"},
{"Qualified name without namespace", "appname", "defaultns", "appname"},
{"Qualified name without namespace and defaultns", "appname", "", "appname"},
}
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
result := InstanceNameFromQualified(tt.appName, tt.defaultNs)
assert.Equal(t, tt.result, result)
})
}
}
func Test_GetRefSources(t *testing.T) {
repoPath, err := filepath.Abs("./../..")
require.NoError(t, err)
getMultiSourceAppSpec := func(sources argoappv1.ApplicationSources) *argoappv1.ApplicationSpec {
return &argoappv1.ApplicationSpec{
Sources: sources,
}
}
repo := &argoappv1.Repository{Repo: "file://" + repoPath}
t.Run("target ref exists", func(t *testing.T) {
argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{
{RepoURL: "file://" + repoPath, Ref: "source-1_2"},
{RepoURL: "file://" + repoPath},
})
refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) {
return repo, nil
}, []string{}, false)
expectedRefSource := argoappv1.RefTargetRevisionMapping{
"$source-1_2": &argoappv1.RefTarget{
Repo: *repo,
},
}
require.NoError(t, err)
assert.Len(t, refSources, 1)
assert.Equal(t, expectedRefSource, refSources)
})
t.Run("target ref does not exist", func(t *testing.T) {
argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{
{RepoURL: "file://does-not-exist", Ref: "source1"},
{RepoURL: "file://" + repoPath},
})
refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) {
return nil, errors.New("repo does not exist")
}, []string{}, false)
require.Error(t, err)
assert.Empty(t, refSources)
})
t.Run("invalid ref", func(t *testing.T) {
argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{
{RepoURL: "file://does-not-exist", Ref: "%invalid-name%"},
{RepoURL: "file://" + repoPath},
})
refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) {
return nil, err
}, []string{}, false)
require.Error(t, err)
assert.Empty(t, refSources)
})
t.Run("duplicate ref keys", func(t *testing.T) {
argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{
{RepoURL: "file://does-not-exist", Ref: "source1"},
{RepoURL: "file://does-not-exist", Ref: "source1"},
})
refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) {
return nil, err
}, []string{}, false)
require.Error(t, err)
assert.Empty(t, refSources)
})
}
func TestValidatePermissionsMultipleSources(t *testing.T) {
t.Run("Empty Repo URL result in condition", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Sources: argoappv1.ApplicationSources{
{RepoURL: ""},
},
}
proj := argoappv1.AppProject{}
db := &dbmocks.ArgoDB{}
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type)
assert.Contains(t, conditions[0].Message, "are required")
})
t.Run("Incomplete Path/Chart/Ref combo result in condition", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Sources: argoappv1.ApplicationSources{
{
RepoURL: "http://some/where",
Path: "",
Chart: "",
Ref: "",
},
},
}
proj := argoappv1.AppProject{}
db := &dbmocks.ArgoDB{}
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type)
assert.Contains(t, conditions[0].Message, "are required")
})
t.Run("One of the Application sources is not permitted in project", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Sources: argoappv1.ApplicationSources{
{
RepoURL: "http://some/where",
Path: "",
Chart: "somechart",
TargetRevision: "1.4.1",
},
},
Destination: argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "testns",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "*",
},
},
SourceRepos: []string{"http://some/where/else"},
},
}
cluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "test"}
db := &dbmocks.ArgoDB{}
db.On("GetCluster", t.Context(), spec.Destination.Server).Return(cluster, nil)
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Len(t, conditions, 1)
assert.Contains(t, conditions[0].Message, "application repo http://some/where is not permitted")
})
t.Run("Source with a Ref field and missing Path/Chart field", func(t *testing.T) {
spec := argoappv1.ApplicationSpec{
Sources: argoappv1.ApplicationSources{
{
RepoURL: "http://some/where",
Path: "",
Chart: "",
Ref: "somechart",
},
},
Destination: argoappv1.ApplicationDestination{
Name: "does-exist",
Namespace: "default",
},
}
proj := argoappv1.AppProject{
Spec: argoappv1.AppProjectSpec{
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "default",
},
},
SourceRepos: []string{"http://some/where"},
},
}
db := &dbmocks.ArgoDB{}
cluster := argoappv1.Cluster{
Name: "does-exist",
Server: "https://127.0.0.1:6443",
}
db.On("GetClusterServersByName", t.Context(), "does-exist").Return([]string{"https://127.0.0.1:6443"}, nil)
db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(&cluster, nil)
conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db)
require.NoError(t, err)
assert.Empty(t, conditions)
})
}
func TestAugmentSyncMsg(t *testing.T) {
mockAPIResourcesFn := func() ([]kube.APIResourceInfo, error) {
return []kube.APIResourceInfo{
{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
GroupVersionResource: schema.GroupVersionResource{
Group: "apps",
Version: "v1",
},
},
{
GroupKind: schema.GroupKind{
Group: "networking.k8s.io",
Kind: "Ingress",
},
GroupVersionResource: schema.GroupVersionResource{
Group: "networking.k8s.io",
Version: "v1",
},
},
}, nil
}
testcases := []struct {
name string
msg string
expectedMessage string
res common.ResourceSyncResult
mockFn func() ([]kube.APIResourceInfo, error)
errMsg string
}{
{
name: "match specific k8s error",
msg: "the server could not find the requested resource",
res: common.ResourceSyncResult{
ResourceKey: kube.ResourceKey{
Name: "deployment-resource",
Namespace: "test-namespace",
Kind: "Deployment",
Group: "apps",
},
Version: "v1beta1",
},
expectedMessage: "The Kubernetes API could not find version \"v1beta1\" of apps/Deployment for requested resource test-namespace/deployment-resource. Version \"v1\" of apps/Deployment is installed on the destination cluster.",
mockFn: mockAPIResourcesFn,
},
{
name: "any random k8s msg",
msg: "random message from k8s",
res: common.ResourceSyncResult{
ResourceKey: kube.ResourceKey{
Name: "deployment-resource",
Namespace: "test-namespace",
Kind: "Deployment",
Group: "apps",
},
Version: "v1beta1",
},
expectedMessage: "random message from k8s",
mockFn: mockAPIResourcesFn,
},
{
name: "resource doesn't exist in the target cluster",
res: common.ResourceSyncResult{
ResourceKey: kube.ResourceKey{
Name: "persistent-volume-resource",
Namespace: "test-namespace",
Kind: "PersistentVolume",
Group: "",
},
Version: "v1",
},
msg: "the server could not find the requested resource",
expectedMessage: "The Kubernetes API could not find /PersistentVolume for requested resource test-namespace/persistent-volume-resource. Make sure the \"PersistentVolume\" CRD is installed on the destination cluster.",
mockFn: mockAPIResourcesFn,
},
{
name: "API Resource returns error",
res: common.ResourceSyncResult{
ResourceKey: kube.ResourceKey{
Name: "persistent-volume-resource",
Namespace: "test-namespace",
Kind: "PersistentVolume",
Group: "",
},
Version: "v1",
},
msg: "the server could not find the requested resource",
expectedMessage: "the server could not find the requested resource",
mockFn: func() ([]kube.APIResourceInfo, error) {
return nil, errors.New("failed to fetch resource of given kind %s from the target cluster")
},
errMsg: "failed to get API resource info for group \"\" and kind \"PersistentVolume\": failed to get API resource info: failed to fetch resource of given kind %s from the target cluster",
},
{
name: "old Ingress type returns error suggesting new Ingress type",
res: common.ResourceSyncResult{
ResourceKey: kube.ResourceKey{
Name: "ingress-resource",
Namespace: "test-namespace",
Kind: "Ingress",
Group: "extensions",
},
Version: "v1beta1",
},
msg: "the server could not find the requested resource",
expectedMessage: "The Kubernetes API could not find version \"v1beta1\" of extensions/Ingress for requested resource test-namespace/ingress-resource. Version \"v1\" of networking.k8s.io/Ingress is installed on the destination cluster.",
mockFn: mockAPIResourcesFn,
},
}
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
tt.res.Message = tt.msg
msg, err := AugmentSyncMsg(tt.res, tt.mockFn)
if tt.errMsg != "" {
assert.EqualError(t, err, tt.errMsg)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedMessage, msg)
}
})
}
}
func TestGetAppEventLabels(t *testing.T) {
tests := []struct {
name string
cmInEventLabelKeys string
cmExEventLabelKeys string
appLabels map[string]string
projLabels map[string]string
expectedEventLabels map[string]string
}{
{
name: "no label keys in cm - no event labels",
cmInEventLabelKeys: "",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: nil,
},
{
name: "label keys in cm, no labels on app & proj - no event labels",
cmInEventLabelKeys: "team, environment",
appLabels: nil,
projLabels: nil,
expectedEventLabels: nil,
},
{
name: "labels on app, no labels on proj - event labels matched on app only",
cmInEventLabelKeys: "team, environment",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: nil,
expectedEventLabels: map[string]string{"team": "A"},
},
{
name: "no labels on app, labels on proj - event labels matched on proj only",
cmInEventLabelKeys: "team, environment",
appLabels: nil,
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"environment": "dev"},
},
{
name: "labels on app & proj with conflicts - event labels matched on both app & proj and app labels prioritized on conflict",
cmInEventLabelKeys: "team, environment",
appLabels: map[string]string{"team": "A", "environment": "stage", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"team": "A", "environment": "stage"},
},
{
name: "wildcard support - matched all labels",
cmInEventLabelKeys: "*",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"team": "A", "tier": "frontend", "environment": "dev"},
},
{
name: "exclude event labels",
cmInEventLabelKeys: "example.com/team,tier,env*",
cmExEventLabelKeys: "tie*",
appLabels: map[string]string{"example.com/team": "A", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"example.com/team": "A", "environment": "dev"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: test.FakeArgoCDNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"resource.includeEventLabelKeys": tt.cmInEventLabelKeys,
"resource.excludeEventLabelKeys": tt.cmExEventLabelKeys,
},
}
proj := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: test.FakeArgoCDNamespace,
Labels: tt.projLabels,
},
}
var app argoappv1.Application
app.Name = "test-app"
app.Namespace = test.FakeArgoCDNamespace
app.Labels = tt.appLabels
appClientset := appclientset.NewSimpleClientset(proj)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
informer := v1alpha1.NewAppProjectInformer(appClientset, test.FakeArgoCDNamespace, 0, indexers)
go informer.Run(ctx.Done())
cache.WaitForCacheSync(ctx.Done(), informer.HasSynced)
kubeClient := fake.NewSimpleClientset(&cm)
settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace)
argoDB := db.NewDB("default", settingsMgr, kubeClient)
eventLabels := GetAppEventLabels(ctx, &app, applisters.NewAppProjectLister(informer.GetIndexer()), test.FakeArgoCDNamespace, settingsMgr, argoDB)
assert.Len(t, eventLabels, len(tt.expectedEventLabels))
for ek, ev := range tt.expectedEventLabels {
v, found := eventLabels[ek]
assert.True(t, found)
assert.Equal(t, ev, v)
}
})
}
}