mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-20 07:18:47 +01:00
* fixed doc comments and added unit tests Signed-off-by: anandf <anjoseph@redhat.com> * Added comments for the newly added unit tests Signed-off-by: anandf <anjoseph@redhat.com> * Refactored method name to deriveServiceAccountToImpersonate Signed-off-by: anandf <anjoseph@redhat.com> * Using const name in return value Signed-off-by: anandf <anjoseph@redhat.com> * Added unit tests for argocd proj add-destination-service-accounts Signed-off-by: anandf <anjoseph@redhat.com> * Fixed failing e2e tests Signed-off-by: anandf <anjoseph@redhat.com> * Fix linting errors Signed-off-by: anandf <anjoseph@redhat.com> * Using require package instead of assert and fixed code generation Signed-off-by: anandf <anjoseph@redhat.com> * Removed parallel execution of tests for sync with impersonate Signed-off-by: anandf <anjoseph@redhat.com> * Added err checks for glob validations Signed-off-by: anandf <anjoseph@redhat.com> * Fixed e2e tests for sync impersonation Signed-off-by: anandf <anjoseph@redhat.com> * Using consistently based expects in E2E tests Signed-off-by: anandf <anjoseph@redhat.com> * Added more unit tests and fixed go generate Signed-off-by: anandf <anjoseph@redhat.com> * Fixed failed lint errors, unit and e2e test failures Signed-off-by: anandf <anjoseph@redhat.com> * Fixed goimports linter issue Signed-off-by: anandf <anjoseph@redhat.com> * Added code comments and added few missing unit tests Signed-off-by: anandf <anjoseph@redhat.com> * Added missing unit test for GetDestinationServiceAccounts method Signed-off-by: anandf <anjoseph@redhat.com> * Fixed goimports formatting with local for project_test.go Signed-off-by: anandf <anjoseph@redhat.com> * Corrected typo in a field name additionalObjs Signed-off-by: anandf <anjoseph@redhat.com> * Fixed failing unit tests Signed-off-by: anandf <anjoseph@redhat.com> --------- Signed-off-by: anandf <anjoseph@redhat.com>
1430 lines
48 KiB
Go
1430 lines
48 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/argoproj/gitops-engine/pkg/sync"
|
|
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
|
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
corev1 "k8s.io/api/core/v1"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"github.com/argoproj/argo-cd/v2/controller/testdata"
|
|
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
|
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
|
"github.com/argoproj/argo-cd/v2/test"
|
|
"github.com/argoproj/argo-cd/v2/util/argo/diff"
|
|
"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
|
|
)
|
|
|
|
func TestPersistRevisionHistory(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
|
|
defaultProject := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, defaultProject},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
|
|
// Sync with source unspecified
|
|
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{},
|
|
}}
|
|
ctrl.appStateManager.SyncAppState(app, opState)
|
|
// Ensure we record spec.source into sync result
|
|
assert.Equal(t, app.Spec.GetSource(), opState.SyncResult.Source)
|
|
|
|
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), app.Name, v1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, updatedApp.Status.History, 1)
|
|
assert.Equal(t, app.Spec.GetSource(), updatedApp.Status.History[0].Source)
|
|
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
|
}
|
|
|
|
func TestPersistManagedNamespaceMetadataState(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
|
|
Labels: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
}
|
|
|
|
defaultProject := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, defaultProject},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
|
|
// Sync with source unspecified
|
|
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{},
|
|
}}
|
|
ctrl.appStateManager.SyncAppState(app, opState)
|
|
// Ensure we record spec.syncPolicy.managedNamespaceMetadata into sync result
|
|
assert.Equal(t, app.Spec.SyncPolicy.ManagedNamespaceMetadata, opState.SyncResult.ManagedNamespaceMetadata)
|
|
}
|
|
|
|
func TestPersistRevisionHistoryRollback(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
defaultProject := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, defaultProject},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
|
|
// Sync with source specified
|
|
source := v1alpha1.ApplicationSource{
|
|
Helm: &v1alpha1.ApplicationSourceHelm{
|
|
Parameters: []v1alpha1.HelmParameter{
|
|
{
|
|
Name: "test",
|
|
Value: "123",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &source,
|
|
},
|
|
}}
|
|
ctrl.appStateManager.SyncAppState(app, opState)
|
|
// Ensure we record opState's source into sync result
|
|
assert.Equal(t, source, opState.SyncResult.Source)
|
|
|
|
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), app.Name, v1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, updatedApp.Status.History, 1)
|
|
assert.Equal(t, source, updatedApp.Status.History[0].Source)
|
|
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
|
}
|
|
|
|
func TestSyncComparisonError(t *testing.T) {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
|
|
defaultProject := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}},
|
|
},
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, defaultProject},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
VerifyResult: "something went wrong",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
|
|
// Sync with source unspecified
|
|
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{},
|
|
}}
|
|
t.Setenv("ARGOCD_GPG_ENABLED", "true")
|
|
ctrl.appStateManager.SyncAppState(app, opState)
|
|
|
|
conditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{v1alpha1.ApplicationConditionComparisonError: true})
|
|
assert.NotEmpty(t, conditions)
|
|
assert.Equal(t, "abc123", opState.SyncResult.Revision)
|
|
}
|
|
|
|
func TestAppStateManager_SyncAppState(t *testing.T) {
|
|
type fixture struct {
|
|
project *v1alpha1.AppProject
|
|
application *v1alpha1.Application
|
|
controller *ApplicationController
|
|
}
|
|
|
|
setup := func() *fixture {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
|
|
project := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}},
|
|
},
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, project},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
|
|
return &fixture{
|
|
project: project,
|
|
application: app,
|
|
controller: ctrl,
|
|
}
|
|
}
|
|
|
|
t.Run("will fail the sync if finds shared resources", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
syncErrorMsg := "deployment already applied by another application"
|
|
condition := v1alpha1.ApplicationCondition{
|
|
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
|
Message: syncErrorMsg,
|
|
}
|
|
f.application.Status.Conditions = append(f.application.Status.Conditions, condition)
|
|
|
|
// Sync with source unspecified
|
|
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &v1alpha1.ApplicationSource{},
|
|
SyncOptions: []string{"FailOnSharedResource=true"},
|
|
},
|
|
}}
|
|
|
|
// when
|
|
f.controller.appStateManager.SyncAppState(f.application, opState)
|
|
|
|
// then
|
|
assert.Equal(t, common.OperationFailed, opState.Phase)
|
|
assert.Contains(t, opState.Message, syncErrorMsg)
|
|
})
|
|
}
|
|
|
|
func TestSyncWindowDeniesSync(t *testing.T) {
|
|
type fixture struct {
|
|
project *v1alpha1.AppProject
|
|
application *v1alpha1.Application
|
|
controller *ApplicationController
|
|
}
|
|
|
|
setup := func() *fixture {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
|
|
project := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
SyncWindows: v1alpha1.SyncWindows{{
|
|
Kind: "deny",
|
|
Schedule: "0 0 * * *",
|
|
Duration: "24h",
|
|
Clusters: []string{"*"},
|
|
Namespaces: []string{"*"},
|
|
Applications: []string{"*"},
|
|
}},
|
|
},
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, project},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: test.FakeClusterURL,
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
|
|
return &fixture{
|
|
project: project,
|
|
application: app,
|
|
controller: ctrl,
|
|
}
|
|
}
|
|
|
|
t.Run("will keep the sync progressing if a sync window prevents the sync", func(t *testing.T) {
|
|
// given a project with an active deny sync window and an operation in progress
|
|
t.Parallel()
|
|
f := setup()
|
|
opMessage := "Sync operation blocked by sync window"
|
|
|
|
opState := &v1alpha1.OperationState{
|
|
Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &v1alpha1.ApplicationSource{},
|
|
},
|
|
},
|
|
Phase: common.OperationRunning,
|
|
}
|
|
// when
|
|
f.controller.appStateManager.SyncAppState(f.application, opState)
|
|
|
|
// then
|
|
assert.Equal(t, common.OperationRunning, opState.Phase)
|
|
assert.Contains(t, opState.Message, opMessage)
|
|
})
|
|
}
|
|
|
|
func TestNormalizeTargetResources(t *testing.T) {
|
|
type fixture struct {
|
|
comparisonResult *comparisonResult
|
|
}
|
|
setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
|
|
t.Helper()
|
|
dc, err := diff.NewDiffConfigBuilder().
|
|
WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
|
|
WithNoCache().
|
|
Build()
|
|
require.NoError(t, err)
|
|
live := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
|
|
target := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
|
|
return &fixture{
|
|
&comparisonResult{
|
|
reconciliationResult: sync.ReconciliationResult{
|
|
Live: []*unstructured.Unstructured{live},
|
|
Target: []*unstructured.Unstructured{target},
|
|
},
|
|
diffConfig: dc,
|
|
},
|
|
}
|
|
}
|
|
t.Run("will modify target resource adding live state in fields it should ignore", func(t *testing.T) {
|
|
// given
|
|
ignore := v1alpha1.ResourceIgnoreDifferences{
|
|
Group: "*",
|
|
Kind: "*",
|
|
ManagedFieldsManagers: []string{"janitor"},
|
|
}
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
|
|
f := setup(t, ignores)
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
iksmVersion := targets[0].GetAnnotations()["iksm-version"]
|
|
assert.Equal(t, "2.0", iksmVersion)
|
|
})
|
|
t.Run("will not modify target resource if ignore difference is not configured", func(t *testing.T) {
|
|
// given
|
|
f := setup(t, []v1alpha1.ResourceIgnoreDifferences{})
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
iksmVersion := targets[0].GetAnnotations()["iksm-version"]
|
|
assert.Equal(t, "1.0", iksmVersion)
|
|
})
|
|
t.Run("will remove fields from target if not present in live", func(t *testing.T) {
|
|
ignore := v1alpha1.ResourceIgnoreDifferences{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
JSONPointers: []string{"/metadata/annotations/iksm-version"},
|
|
}
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
|
|
f := setup(t, ignores)
|
|
live := f.comparisonResult.reconciliationResult.Live[0]
|
|
unstructured.RemoveNestedField(live.Object, "metadata", "annotations", "iksm-version")
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
_, ok := targets[0].GetAnnotations()["iksm-version"]
|
|
assert.False(t, ok)
|
|
})
|
|
t.Run("will correctly normalize with multiple ignore configurations", func(t *testing.T) {
|
|
// given
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
|
{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
JSONPointers: []string{"/spec/replicas"},
|
|
},
|
|
{
|
|
Group: "*",
|
|
Kind: "*",
|
|
ManagedFieldsManagers: []string{"janitor"},
|
|
},
|
|
}
|
|
f := setup(t, ignores)
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
normalized := targets[0]
|
|
iksmVersion, ok := normalized.GetAnnotations()["iksm-version"]
|
|
require.True(t, ok)
|
|
assert.Equal(t, "2.0", iksmVersion)
|
|
replicas, ok, err := unstructured.NestedInt64(normalized.Object, "spec", "replicas")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, int64(4), replicas)
|
|
})
|
|
t.Run("will keep new array entries not found in live state if not ignored", func(t *testing.T) {
|
|
t.Skip("limitation in the current implementation")
|
|
// given
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
|
{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"guestbook-ui\")"},
|
|
},
|
|
}
|
|
f := setup(t, ignores)
|
|
target := test.YamlToUnstructured(testdata.TargetDeploymentNewEntries)
|
|
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Len(t, containers, 2)
|
|
})
|
|
}
|
|
|
|
func TestNormalizeTargetResourcesWithList(t *testing.T) {
|
|
type fixture struct {
|
|
comparisonResult *comparisonResult
|
|
}
|
|
setupHttpProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
|
|
t.Helper()
|
|
dc, err := diff.NewDiffConfigBuilder().
|
|
WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
|
|
WithNoCache().
|
|
Build()
|
|
require.NoError(t, err)
|
|
live := test.YamlToUnstructured(testdata.LiveHTTPProxy)
|
|
target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
|
|
return &fixture{
|
|
&comparisonResult{
|
|
reconciliationResult: sync.ReconciliationResult{
|
|
Live: []*unstructured.Unstructured{live},
|
|
Target: []*unstructured.Unstructured{target},
|
|
},
|
|
diffConfig: dc,
|
|
},
|
|
}
|
|
}
|
|
|
|
t.Run("will properly ignore nested fields within arrays", func(t *testing.T) {
|
|
// given
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
|
{
|
|
Group: "projectcontour.io",
|
|
Kind: "HTTPProxy",
|
|
JQPathExpressions: []string{".spec.routes[]"},
|
|
// JSONPointers: []string{"/spec/routes"},
|
|
},
|
|
}
|
|
f := setupHttpProxy(t, ignores)
|
|
target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
|
|
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
|
|
|
// when
|
|
patchedTargets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, f.comparisonResult.reconciliationResult.Live, 1)
|
|
require.Len(t, f.comparisonResult.reconciliationResult.Target, 1)
|
|
require.Len(t, patchedTargets, 1)
|
|
|
|
// live should have 1 entry
|
|
require.Len(t, dig[[]any](f.comparisonResult.reconciliationResult.Live[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"}), 1)
|
|
// assert some arbitrary field to show `entries[0]` is not an empty object
|
|
require.Equal(t, "sample-header", dig[string](f.comparisonResult.reconciliationResult.Live[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeader", "headerName"}))
|
|
|
|
// target has 2 entries
|
|
require.Len(t, dig[[]any](f.comparisonResult.reconciliationResult.Target[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries"}), 2)
|
|
// assert some arbitrary field to show `entries[0]` is not an empty object
|
|
require.Equal(t, "sample-header", dig[string](f.comparisonResult.reconciliationResult.Target[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeaderValueMatch", "headers", 0, "name"}))
|
|
|
|
// It should be *1* entries in the array
|
|
require.Len(t, dig[[]any](patchedTargets[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"}), 1)
|
|
// and it should NOT equal an empty object
|
|
require.Len(t, dig[any](patchedTargets[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0}), 1)
|
|
})
|
|
t.Run("will correctly set array entries if new entries have been added", func(t *testing.T) {
|
|
// given
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
|
{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
JQPathExpressions: []string{".spec.template.spec.containers[].env[] | select(.name == \"SOME_ENV_VAR\")"},
|
|
},
|
|
}
|
|
f := setupHttpProxy(t, ignores)
|
|
live := test.YamlToUnstructured(testdata.LiveDeploymentEnvVarsYaml)
|
|
target := test.YamlToUnstructured(testdata.TargetDeploymentEnvVarsYaml)
|
|
f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
|
|
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Len(t, containers, 1)
|
|
|
|
ports := containers[0].(map[string]interface{})["ports"].([]interface{})
|
|
assert.Len(t, ports, 1)
|
|
|
|
env := containers[0].(map[string]interface{})["env"].([]interface{})
|
|
assert.Len(t, env, 3)
|
|
|
|
first := env[0]
|
|
second := env[1]
|
|
third := env[2]
|
|
|
|
// Currently the defined order at this time is the insertion order of the target manifest.
|
|
assert.Equal(t, "SOME_ENV_VAR", first.(map[string]interface{})["name"])
|
|
assert.Equal(t, "some_value", first.(map[string]interface{})["value"])
|
|
|
|
assert.Equal(t, "SOME_OTHER_ENV_VAR", second.(map[string]interface{})["name"])
|
|
assert.Equal(t, "some_other_value", second.(map[string]interface{})["value"])
|
|
|
|
assert.Equal(t, "YET_ANOTHER_ENV_VAR", third.(map[string]interface{})["name"])
|
|
assert.Equal(t, "yet_another_value", third.(map[string]interface{})["value"])
|
|
})
|
|
|
|
t.Run("ignore-deployment-image-replicas-changes-additive", func(t *testing.T) {
|
|
// given
|
|
|
|
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
|
{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
JSONPointers: []string{"/spec/replicas"},
|
|
}, {
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
JQPathExpressions: []string{".spec.template.spec.containers[].image"},
|
|
},
|
|
}
|
|
f := setupHttpProxy(t, ignores)
|
|
live := test.YamlToUnstructured(testdata.MinimalImageReplicaDeploymentYaml)
|
|
target := test.YamlToUnstructured(testdata.AdditionalImageReplicaDeploymentYaml)
|
|
f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
|
|
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
|
|
|
// when
|
|
targets, err := normalizeTargetResources(f.comparisonResult)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
metadata, ok, err := unstructured.NestedMap(targets[0].Object, "metadata")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
labels, ok := metadata["labels"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Len(t, labels, 2)
|
|
assert.Equal(t, "web", labels["appProcess"])
|
|
|
|
spec, ok, err := unstructured.NestedMap(targets[0].Object, "spec")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
assert.Equal(t, int64(1), spec["replicas"])
|
|
|
|
template, ok := spec["template"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
|
|
tMetadata, ok := template["metadata"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
tLabels, ok := tMetadata["labels"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Len(t, tLabels, 2)
|
|
assert.Equal(t, "web", tLabels["appProcess"])
|
|
|
|
tSpec, ok := template["spec"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
containers, ok, err := unstructured.NestedSlice(tSpec, "containers")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Len(t, containers, 1)
|
|
|
|
first := containers[0].(map[string]interface{})
|
|
assert.Equal(t, "alpine:3", first["image"])
|
|
|
|
resources, ok := first["resources"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
requests, ok := resources["requests"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, "400m", requests["cpu"])
|
|
|
|
env, ok, err := unstructured.NestedSlice(first, "env")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Len(t, env, 1)
|
|
|
|
env0 := env[0].(map[string]interface{})
|
|
assert.Equal(t, "EV", env0["name"])
|
|
assert.Equal(t, "here", env0["value"])
|
|
})
|
|
}
|
|
|
|
func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
|
type fixture struct {
|
|
project *v1alpha1.AppProject
|
|
application *v1alpha1.Application
|
|
}
|
|
|
|
setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
|
|
project := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "argocd-ns",
|
|
Name: "testProj",
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
DestinationServiceAccounts: destinationServiceAccounts,
|
|
},
|
|
}
|
|
app := &v1alpha1.Application{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: applicationNamespace,
|
|
Name: "testApp",
|
|
},
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
Project: "testProj",
|
|
Destination: v1alpha1.ApplicationDestination{
|
|
Server: destinationServerURL,
|
|
Namespace: destinationNamespace,
|
|
},
|
|
},
|
|
}
|
|
return &fixture{
|
|
project: project,
|
|
application: app,
|
|
}
|
|
}
|
|
|
|
t.Run("empty destination service accounts", func(t *testing.T) {
|
|
// given an application referring a project with no destination service accounts
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := ""
|
|
expectedErrMsg := "no matching service account found for destination server https://kubernetes.svc.local and namespace testns"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
assert.Equal(t, expectedSA, sa)
|
|
|
|
// then, there should be an error saying no valid match was found
|
|
assert.EqualError(t, err, expectedErrMsg)
|
|
})
|
|
|
|
t.Run("exact match of destination namespace", func(t *testing.T) {
|
|
// given an application referring a project with exactly one destination service account that matches the application destination,
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should be no error and should use the right service account for impersonation
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts having one exact match for application destination
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "guestbook",
|
|
DefaultServiceAccount: "guestbook-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "guestbook-test",
|
|
DefaultServiceAccount: "guestbook-test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should be no error and should use the right service account for impersonation
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts having multiple match for application destination
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa-3",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "guestbook",
|
|
DefaultServiceAccount: "guestbook-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should be no error and it should use the first matching service account for impersonation
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts with glob patterns matching the application destination
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "test*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and should use the first matching glob pattern service account for impersonation
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("no match among a valid list", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts with no matches for application destination
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "test1",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "test2",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := ""
|
|
expectedErrMsg := "no matching service account found for destination server https://kubernetes.svc.local and namespace testns"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should be an error saying no match was found
|
|
require.EqualError(t, err, expectedErrMsg)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("app destination namespace is empty", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts with empty application destination namespace
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "*",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
}
|
|
destinationNamespace := ""
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:argocd-ns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and the service account configured for with empty namespace should be used.
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("match done via catch all glob pattern", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts having a catch all glob pattern
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns1",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and the catch all service account should be returned
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("match done via invalid glob pattern", func(t *testing.T) {
|
|
// given an application referring a project with a destination service account having an invalid glob pattern for namespace
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "e[[a*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := ""
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there must be an error as the glob pattern is invalid.
|
|
require.ErrorContains(t, err, "invalid glob pattern for destination namespace")
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("sa specified with a namespace", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts having a matching service account specified with its namespace
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "myns:test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:myns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
assert.Equal(t, expectedSA, sa)
|
|
|
|
// then, there should not be any error and the service account with its namespace should be returned.
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
|
type fixture struct {
|
|
project *v1alpha1.AppProject
|
|
application *v1alpha1.Application
|
|
}
|
|
|
|
setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
|
|
project := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: "argocd-ns",
|
|
Name: "testProj",
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
DestinationServiceAccounts: destinationServiceAccounts,
|
|
},
|
|
}
|
|
app := &v1alpha1.Application{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: applicationNamespace,
|
|
Name: "testApp",
|
|
},
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
Project: "testProj",
|
|
Destination: v1alpha1.ApplicationDestination{
|
|
Server: destinationServerURL,
|
|
Namespace: destinationNamespace,
|
|
},
|
|
},
|
|
}
|
|
return &fixture{
|
|
project: project,
|
|
application: app,
|
|
}
|
|
}
|
|
|
|
t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts and one exact match for application destination
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "guestbook",
|
|
DefaultServiceAccount: "guestbook-sa",
|
|
},
|
|
{
|
|
Server: "https://abc.svc.local",
|
|
Namespace: "guestbook",
|
|
DefaultServiceAccount: "guestbook-test-sa",
|
|
},
|
|
{
|
|
Server: "https://cde.svc.local",
|
|
Namespace: "guestbook",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and the right service account must be returned.
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts and multiple matches for application destination
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "guestbook",
|
|
DefaultServiceAccount: "guestbook-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and first matching service account should be used
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts with a matching glob pattern and exact match
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "test*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
assert.Equal(t, expectedSA, sa)
|
|
|
|
// then, there should not be any error and the service account of the glob pattern, being the first match should be returned.
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("no match among a valid list", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts with no match
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
{
|
|
Server: "https://abc.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://cde.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://xyz.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := ""
|
|
expectedErr := "no matching service account found for destination server https://xyz.svc.local and namespace testns"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there an error with appropriate message must be returned
|
|
require.EqualError(t, err, expectedErr)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("match done via catch all glob pattern", func(t *testing.T) {
|
|
// given an application referring a project with multiple destination service accounts with matching catch all glob pattern
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "testns1",
|
|
DefaultServiceAccount: "test-sa-2",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "*",
|
|
Namespace: "*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://localhost:6443"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:testns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and the service account of the glob pattern match must be returned.
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("match done via invalid glob pattern", func(t *testing.T) {
|
|
// given an application referring a project with a destination service account having an invalid glob pattern for server
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "e[[a*",
|
|
Namespace: "test-ns",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://kubernetes.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := ""
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there must be an error as the glob pattern is invalid.
|
|
require.ErrorContains(t, err, "invalid glob pattern for destination server")
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
|
|
t.Run("sa specified with a namespace", func(t *testing.T) {
|
|
// given app sync impersonation feature is enabled and matching service account is prefixed with a namespace
|
|
t.Parallel()
|
|
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://abc.svc.local",
|
|
Namespace: "testns",
|
|
DefaultServiceAccount: "myns:test-sa",
|
|
},
|
|
{
|
|
Server: "https://kubernetes.svc.local",
|
|
Namespace: "default",
|
|
DefaultServiceAccount: "default-sa",
|
|
},
|
|
{
|
|
Server: "*",
|
|
Namespace: "*",
|
|
DefaultServiceAccount: "test-sa",
|
|
},
|
|
}
|
|
destinationNamespace := "testns"
|
|
destinationServerURL := "https://abc.svc.local"
|
|
applicationNamespace := "argocd-ns"
|
|
expectedSA := "system:serviceaccount:myns:test-sa"
|
|
|
|
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
|
// when
|
|
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
|
|
|
|
// then, there should not be any error and the service account with the given namespace prefix must be returned.
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedSA, sa)
|
|
})
|
|
}
|
|
|
|
func TestSyncWithImpersonate(t *testing.T) {
|
|
type fixture struct {
|
|
project *v1alpha1.AppProject
|
|
application *v1alpha1.Application
|
|
controller *ApplicationController
|
|
}
|
|
|
|
setup := func(impersonationEnabled bool, destinationNamespace, serviceAccountName string) *fixture {
|
|
app := newFakeApp()
|
|
app.Status.OperationState = nil
|
|
app.Status.History = nil
|
|
project := &v1alpha1.AppProject{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Namespace: test.FakeArgoCDNamespace,
|
|
Name: "default",
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
DestinationServiceAccounts: []v1alpha1.
|
|
ApplicationDestinationServiceAccount{
|
|
{
|
|
Server: "https://localhost:6443",
|
|
Namespace: destinationNamespace,
|
|
DefaultServiceAccount: serviceAccountName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
additionalObjs := []runtime.Object{}
|
|
if serviceAccountName != "" {
|
|
syncServiceAccount := &corev1.ServiceAccount{
|
|
ObjectMeta: v1.ObjectMeta{
|
|
Name: serviceAccountName,
|
|
Namespace: test.FakeDestNamespace,
|
|
},
|
|
}
|
|
additionalObjs = append(additionalObjs, syncServiceAccount)
|
|
}
|
|
data := fakeData{
|
|
apps: []runtime.Object{app, project},
|
|
manifestResponse: &apiclient.ManifestResponse{
|
|
Manifests: []string{},
|
|
Namespace: test.FakeDestNamespace,
|
|
Server: "https://localhost:6443",
|
|
Revision: "abc123",
|
|
},
|
|
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
|
|
configMapData: map[string]string{
|
|
"application.sync.impersonation.enabled": strconv.FormatBool(impersonationEnabled),
|
|
},
|
|
additionalObjs: additionalObjs,
|
|
}
|
|
ctrl := newFakeController(&data, nil)
|
|
return &fixture{
|
|
project: project,
|
|
application: app,
|
|
controller: ctrl,
|
|
}
|
|
}
|
|
|
|
t.Run("sync with impersonation and no matching service account", func(t *testing.T) {
|
|
// given app sync impersonation feature is enabled with an application referring a project no matching service account
|
|
f := setup(true, test.FakeArgoCDNamespace, "")
|
|
opMessage := "failed to find a matching service account to impersonate: no matching service account found for destination server https://localhost:6443 and namespace fake-dest-ns"
|
|
|
|
opState := &v1alpha1.OperationState{
|
|
Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &v1alpha1.ApplicationSource{},
|
|
},
|
|
},
|
|
Phase: common.OperationRunning,
|
|
}
|
|
// when
|
|
f.controller.appStateManager.SyncAppState(f.application, opState)
|
|
|
|
// then, app sync should fail with expected error message in operation state
|
|
assert.Equal(t, common.OperationError, opState.Phase)
|
|
assert.Contains(t, opState.Message, opMessage)
|
|
})
|
|
|
|
t.Run("sync with impersonation and empty service account match", func(t *testing.T) {
|
|
// given app sync impersonation feature is enabled with an application referring a project matching service account that is an empty string
|
|
f := setup(true, test.FakeDestNamespace, "")
|
|
opMessage := "failed to find a matching service account to impersonate: default service account contains invalid chars ''"
|
|
|
|
opState := &v1alpha1.OperationState{
|
|
Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &v1alpha1.ApplicationSource{},
|
|
},
|
|
},
|
|
Phase: common.OperationRunning,
|
|
}
|
|
// when
|
|
f.controller.appStateManager.SyncAppState(f.application, opState)
|
|
|
|
// then app sync should fail with expected error message in operation state
|
|
assert.Equal(t, common.OperationError, opState.Phase)
|
|
assert.Contains(t, opState.Message, opMessage)
|
|
})
|
|
|
|
t.Run("sync with impersonation and matching sa", func(t *testing.T) {
|
|
// given app sync impersonation feature is enabled with an application referring a project matching service account
|
|
f := setup(true, test.FakeDestNamespace, "test-sa")
|
|
opMessage := "successfully synced (no more tasks)"
|
|
|
|
opState := &v1alpha1.OperationState{
|
|
Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &v1alpha1.ApplicationSource{},
|
|
},
|
|
},
|
|
Phase: common.OperationRunning,
|
|
}
|
|
// when
|
|
f.controller.appStateManager.SyncAppState(f.application, opState)
|
|
|
|
// then app sync should not fail
|
|
assert.Equal(t, common.OperationSucceeded, opState.Phase)
|
|
assert.Contains(t, opState.Message, opMessage)
|
|
})
|
|
|
|
t.Run("sync without impersonation", func(t *testing.T) {
|
|
// given app sync impersonation feature is disabled with an application referring a project matching service account
|
|
f := setup(false, test.FakeDestNamespace, "")
|
|
opMessage := "successfully synced (no more tasks)"
|
|
|
|
opState := &v1alpha1.OperationState{
|
|
Operation: v1alpha1.Operation{
|
|
Sync: &v1alpha1.SyncOperation{
|
|
Source: &v1alpha1.ApplicationSource{},
|
|
},
|
|
},
|
|
Phase: common.OperationRunning,
|
|
}
|
|
// when
|
|
f.controller.appStateManager.SyncAppState(f.application, opState)
|
|
|
|
// then application sync should pass using the control plane service account
|
|
assert.Equal(t, common.OperationSucceeded, opState.Phase)
|
|
assert.Contains(t, opState.Message, opMessage)
|
|
})
|
|
}
|
|
|
|
func dig[T any](obj interface{}, path []interface{}) T {
|
|
i := obj
|
|
|
|
for _, segment := range path {
|
|
switch segment.(type) {
|
|
case int:
|
|
i = i.([]interface{})[segment.(int)]
|
|
case string:
|
|
i = i.(map[string]interface{})[segment.(string)]
|
|
default:
|
|
panic("invalid path for object")
|
|
}
|
|
}
|
|
|
|
return i.(T)
|
|
}
|