mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-04-03 07:18:49 +02:00
Compare commits
7 Commits
renovate/n
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa0045311 | ||
|
|
44e08631f2 | ||
|
|
62670d6595 | ||
|
|
fabbbbe6ee | ||
|
|
3eebbcb33b | ||
|
|
4259f467b0 | ||
|
|
32f23a446f |
2
.github/workflows/ci-build.yaml
vendored
2
.github/workflows/ci-build.yaml
vendored
@@ -423,7 +423,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # v7.0.0
|
||||
uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4 # v7.1.0
|
||||
if: env.sonar_secret != ''
|
||||
test-e2e:
|
||||
name: Run end-to-end tests
|
||||
|
||||
2
Makefile
2
Makefile
@@ -487,7 +487,7 @@ test-e2e:
|
||||
test-e2e-local: cli-local
|
||||
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
|
||||
export GO111MODULE=off
|
||||
DIST_DIR=${DIST_DIR} RERUN_FAILS=$(ARGOCD_E2E_RERUN_FAILS) PACKAGES="./test/e2e" ARGOCD_E2E_RECORD=${ARGOCD_E2E_RECORD} ARGOCD_CONFIG_DIR=$(HOME)/.config/argocd-e2e ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout $(ARGOCD_E2E_TEST_TIMEOUT) -v -args -test.gocoverdir="$(PWD)/test-results"
|
||||
ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS=$${ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS:-true} DIST_DIR=${DIST_DIR} RERUN_FAILS=$(ARGOCD_E2E_RERUN_FAILS) PACKAGES="./test/e2e" ARGOCD_E2E_RECORD=${ARGOCD_E2E_RECORD} ARGOCD_CONFIG_DIR=$(HOME)/.config/argocd-e2e ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout $(ARGOCD_E2E_TEST_TIMEOUT) -v -args -test.gocoverdir="$(PWD)/test-results"
|
||||
|
||||
# Spawns a shell in the test server container for debugging purposes
|
||||
debug-test-server: test-tools-image
|
||||
|
||||
2
Procfile
2
Procfile
@@ -10,5 +10,5 @@ git-server: test/fixture/testrepos/start-git.sh
|
||||
helm-registry: test/fixture/testrepos/start-helm-registry.sh
|
||||
oci-registry: test/fixture/testrepos/start-authenticated-helm-registry.sh
|
||||
dev-mounter: [ "$ARGOCD_E2E_TEST" != "true" ] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
|
||||
applicationset-controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/applicationset-controller} FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-applicationset-controller $COMMAND --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
applicationset-controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/applicationset-controller} FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-applicationset-controller ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS=${ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS:-true} $COMMAND --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/notification} FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --self-service-notification-enabled=${ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED:-'false'}"
|
||||
|
||||
@@ -2690,7 +2690,7 @@ func (ctrl *ApplicationController) applyImpersonationConfig(config *rest.Config,
|
||||
if !impersonationEnabled {
|
||||
return nil
|
||||
}
|
||||
user, err := deriveServiceAccountToImpersonate(proj, app, destCluster)
|
||||
user, err := settings_util.DeriveServiceAccountToImpersonate(proj, app, destCluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving service account to impersonate: %w", err)
|
||||
}
|
||||
|
||||
@@ -132,11 +132,11 @@ func (c *clusterInfoUpdater) getUpdatedClusterInfo(ctx context.Context, apps []*
|
||||
continue
|
||||
}
|
||||
}
|
||||
destCluster, err := argo.GetDestinationCluster(ctx, a.Spec.Destination, c.db)
|
||||
destServer, err := argo.GetDestinationServer(ctx, a.Spec.Destination, c.db)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if destCluster.Server == cluster.Server {
|
||||
if destServer == cluster.Server {
|
||||
appCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,121 @@ func TestClusterSecretUpdater(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUpdatedClusterInfo_AppCount(t *testing.T) {
|
||||
const fakeNamespace = "fake-ns"
|
||||
const clusterServer = "https://prod.example.com"
|
||||
const clusterName = "prod"
|
||||
|
||||
emptyArgoCDConfigMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
},
|
||||
Data: map[string]string{},
|
||||
}
|
||||
argoCDSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
},
|
||||
Data: map[string][]byte{"admin.password": nil, "server.secretkey": nil},
|
||||
}
|
||||
clusterSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "prod-cluster",
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeCluster},
|
||||
Annotations: map[string]string{
|
||||
common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"name": []byte(clusterName),
|
||||
"server": []byte(clusterServer),
|
||||
"config": []byte("{}"),
|
||||
},
|
||||
}
|
||||
|
||||
kubeclientset := fake.NewClientset(emptyArgoCDConfigMap, argoCDSecret, clusterSecret)
|
||||
settingsManager := settings.NewSettingsManager(t.Context(), kubeclientset, fakeNamespace)
|
||||
argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)
|
||||
|
||||
apps := []*v1alpha1.Application{
|
||||
{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Name: clusterName}}},
|
||||
{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Server: clusterServer}}},
|
||||
{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Server: "https://other.example.com"}}},
|
||||
}
|
||||
|
||||
updater := &clusterInfoUpdater{db: argoDB, namespace: fakeNamespace}
|
||||
cluster := v1alpha1.Cluster{Server: clusterServer}
|
||||
|
||||
info := updater.getUpdatedClusterInfo(t.Context(), apps, cluster, nil, metav1.Now())
|
||||
|
||||
assert.Equal(t, int64(2), info.ApplicationsCount)
|
||||
}
|
||||
|
||||
func TestGetUpdatedClusterInfo_AmbiguousName(t *testing.T) {
|
||||
const fakeNamespace = "fake-ns"
|
||||
const clusterServer = "https://prod.example.com"
|
||||
const clusterName = "prod"
|
||||
|
||||
emptyArgoCDConfigMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
},
|
||||
Data: map[string]string{},
|
||||
}
|
||||
argoCDSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
},
|
||||
Data: map[string][]byte{"admin.password": nil, "server.secretkey": nil},
|
||||
}
|
||||
makeClusterSecret := func(secretName, server string) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeCluster},
|
||||
Annotations: map[string]string{
|
||||
common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"name": []byte(clusterName),
|
||||
"server": []byte(server),
|
||||
"config": []byte("{}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Two secrets share the same cluster name
|
||||
kubeclientset := fake.NewClientset(
|
||||
emptyArgoCDConfigMap, argoCDSecret,
|
||||
makeClusterSecret("prod-cluster-1", clusterServer),
|
||||
makeClusterSecret("prod-cluster-2", "https://prod2.example.com"),
|
||||
)
|
||||
settingsManager := settings.NewSettingsManager(t.Context(), kubeclientset, fakeNamespace)
|
||||
argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)
|
||||
|
||||
apps := []*v1alpha1.Application{
|
||||
{Spec: v1alpha1.ApplicationSpec{Destination: v1alpha1.ApplicationDestination{Name: clusterName}}},
|
||||
}
|
||||
|
||||
updater := &clusterInfoUpdater{db: argoDB, namespace: fakeNamespace}
|
||||
cluster := v1alpha1.Cluster{Server: clusterServer}
|
||||
|
||||
info := updater.getUpdatedClusterInfo(t.Context(), apps, cluster, nil, metav1.Now())
|
||||
|
||||
assert.Equal(t, int64(0), info.ApplicationsCount, "ambiguous name should not count app")
|
||||
}
|
||||
|
||||
func TestUpdateClusterLabels(t *testing.T) {
|
||||
shouldNotBeInvoked := func(_ context.Context, _ *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
|
||||
shouldNotHappen := errors.New("if an error happens here, something's wrong")
|
||||
|
||||
@@ -847,11 +847,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
||||
if err != nil {
|
||||
log.Errorf("CompareAppState error getting server side diff dry run applier: %s", err)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionUnknownError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
if cleanup != nil {
|
||||
} else {
|
||||
defer cleanup()
|
||||
diffConfigBuilder.WithServerSideDryRunner(diff.NewK8sServerSideDryRunner(applier))
|
||||
}
|
||||
diffConfigBuilder.WithServerSideDryRunner(diff.NewK8sServerSideDryRunner(applier))
|
||||
}
|
||||
|
||||
// enable structured merge diff if application syncs with server-side apply
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
@@ -33,20 +32,16 @@ import (
|
||||
applog "github.com/argoproj/argo-cd/v3/util/app/log"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/diff"
|
||||
"github.com/argoproj/argo-cd/v3/util/glob"
|
||||
kubeutil "github.com/argoproj/argo-cd/v3/util/kube"
|
||||
logutils "github.com/argoproj/argo-cd/v3/util/log"
|
||||
"github.com/argoproj/argo-cd/v3/util/lua"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvVarSyncWaveDelay is an environment variable which controls the delay in seconds between
|
||||
// each sync-wave
|
||||
EnvVarSyncWaveDelay = "ARGOCD_SYNC_WAVE_DELAY"
|
||||
|
||||
// serviceAccountDisallowedCharSet contains the characters that are not allowed to be present
|
||||
// in a DefaultServiceAccount configured for a DestinationServiceAccount
|
||||
serviceAccountDisallowedCharSet = "!*[]{}\\/"
|
||||
)
|
||||
|
||||
func (m *appStateManager) getOpenAPISchema(server *v1alpha1.Cluster) (openapi.Resources, error) {
|
||||
@@ -288,7 +283,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, project *v1alp
|
||||
return
|
||||
}
|
||||
if impersonationEnabled {
|
||||
serviceAccountToImpersonate, err := deriveServiceAccountToImpersonate(project, app, destCluster)
|
||||
serviceAccountToImpersonate, err := settings.DeriveServiceAccountToImpersonate(project, app, destCluster)
|
||||
if err != nil {
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("failed to find a matching service account to impersonate: %v", err)
|
||||
@@ -558,41 +553,6 @@ func syncWindowPreventsSync(app *v1alpha1.Application, proj *v1alpha1.AppProject
|
||||
return !canSync, nil
|
||||
}
|
||||
|
||||
// deriveServiceAccountToImpersonate determines the service account to be used for impersonation for the sync operation.
|
||||
// The returned service account will be fully qualified including namespace and the service account name in the format system:serviceaccount:<namespace>:<service_account>
|
||||
func deriveServiceAccountToImpersonate(project *v1alpha1.AppProject, application *v1alpha1.Application, destCluster *v1alpha1.Cluster) (string, error) {
|
||||
// spec.Destination.Namespace is optional. If not specified, use the Application's
|
||||
// namespace
|
||||
serviceAccountNamespace := application.Spec.Destination.Namespace
|
||||
if serviceAccountNamespace == "" {
|
||||
serviceAccountNamespace = application.Namespace
|
||||
}
|
||||
// Loop through the destinationServiceAccounts and see if there is any destination that is a candidate.
|
||||
// if so, return the service account specified for that destination.
|
||||
for _, item := range project.Spec.DestinationServiceAccounts {
|
||||
dstServerMatched, err := glob.MatchWithError(item.Server, destCluster.Server)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid glob pattern for destination server: %w", err)
|
||||
}
|
||||
dstNamespaceMatched, err := glob.MatchWithError(item.Namespace, application.Spec.Destination.Namespace)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid glob pattern for destination namespace: %w", err)
|
||||
}
|
||||
if dstServerMatched && dstNamespaceMatched {
|
||||
if strings.Trim(item.DefaultServiceAccount, " ") == "" || strings.ContainsAny(item.DefaultServiceAccount, serviceAccountDisallowedCharSet) {
|
||||
return "", fmt.Errorf("default service account contains invalid chars '%s'", item.DefaultServiceAccount)
|
||||
} else if strings.Contains(item.DefaultServiceAccount, ":") {
|
||||
// service account is specified along with its namespace.
|
||||
return "system:serviceaccount:" + item.DefaultServiceAccount, nil
|
||||
}
|
||||
// service account needs to be prefixed with a namespace
|
||||
return fmt.Sprintf("system:serviceaccount:%s:%s", serviceAccountNamespace, item.DefaultServiceAccount), nil
|
||||
}
|
||||
}
|
||||
// if there is no match found in the AppProject.Spec.DestinationServiceAccounts, use the default service account of the destination namespace.
|
||||
return "", fmt.Errorf("no matching service account found for destination server %s and namespace %s", application.Spec.Destination.Server, serviceAccountNamespace)
|
||||
}
|
||||
|
||||
// validateSyncPermissions checks whether the given resource is permitted by the project's
|
||||
// allow/deny lists and destination rules. It returns an error if the API resource info is nil
|
||||
// (preventing a nil-pointer panic), if the resource's group/kind is not permitted, or if
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/test"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/diff"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
)
|
||||
|
||||
func TestPersistRevisionHistory(t *testing.T) {
|
||||
@@ -726,7 +727,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
assert.Equal(t, expectedSA, sa)
|
||||
|
||||
// then, there should be an error saying no valid match was found
|
||||
@@ -750,7 +751,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should be no error and should use the right service account for impersonation
|
||||
require.NoError(t, err)
|
||||
@@ -789,7 +790,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should be no error and should use the right service account for impersonation
|
||||
require.NoError(t, err)
|
||||
@@ -828,7 +829,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should be no error and it should use the first matching service account for impersonation
|
||||
require.NoError(t, err)
|
||||
@@ -862,7 +863,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should not be any error and should use the first matching glob pattern service account for impersonation
|
||||
require.NoError(t, err)
|
||||
@@ -897,7 +898,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should be an error saying no match was found
|
||||
require.EqualError(t, err, expectedErrMsg)
|
||||
@@ -925,7 +926,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should not be any error and the service account configured for with empty namespace should be used.
|
||||
require.NoError(t, err)
|
||||
@@ -959,7 +960,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should not be any error and the catch all service account should be returned
|
||||
require.NoError(t, err)
|
||||
@@ -983,7 +984,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there must be an error as the glob pattern is invalid.
|
||||
require.ErrorContains(t, err, "invalid glob pattern for destination namespace")
|
||||
@@ -1017,7 +1018,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
assert.Equal(t, expectedSA, sa)
|
||||
|
||||
// then, there should not be any error and the service account with its namespace should be returned.
|
||||
@@ -1045,7 +1046,7 @@ func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
|
||||
f.application.Spec.Destination.Name = f.cluster.Name
|
||||
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
assert.Equal(t, expectedSA, sa)
|
||||
|
||||
// then, there should not be any error and the service account with its namespace should be returned.
|
||||
@@ -1128,7 +1129,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should not be any error and the right service account must be returned.
|
||||
require.NoError(t, err)
|
||||
@@ -1167,7 +1168,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should not be any error and first matching service account should be used
|
||||
require.NoError(t, err)
|
||||
@@ -1201,7 +1202,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
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.
|
||||
@@ -1236,7 +1237,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})
|
||||
|
||||
// then, there an error with appropriate message must be returned
|
||||
require.EqualError(t, err, expectedErr)
|
||||
@@ -1270,7 +1271,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there should not be any error and the service account of the glob pattern match must be returned.
|
||||
require.NoError(t, err)
|
||||
@@ -1294,7 +1295,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
|
||||
// then, there must be an error as the glob pattern is invalid.
|
||||
require.ErrorContains(t, err, "invalid glob pattern for destination server")
|
||||
@@ -1328,7 +1329,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
|
||||
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, &v1alpha1.Cluster{Server: destinationServerURL})
|
||||
|
||||
// then, there should not be any error and the service account with the given namespace prefix must be returned.
|
||||
require.NoError(t, err)
|
||||
@@ -1356,7 +1357,7 @@ func TestDeriveServiceAccountMatchingServers(t *testing.T) {
|
||||
f.application.Spec.Destination.Name = f.cluster.Name
|
||||
|
||||
// when
|
||||
sa, err := deriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
sa, err := settings.DeriveServiceAccountToImpersonate(f.project, f.application, f.cluster)
|
||||
assert.Equal(t, expectedSA, sa)
|
||||
|
||||
// then, there should not be any error and the service account with its namespace should be returned.
|
||||
|
||||
@@ -30,7 +30,7 @@ Impersonation requests first authenticate as the requesting user, then switch to
|
||||
|
||||
### Feature scope
|
||||
|
||||
Impersonation is currently only supported for the lifecycle of objects managed by an Application directly, which includes sync operations (creation, update and pruning of resources) and deletion as part of Application finalizer logic. This *does not* includes operations triggered via ArgoCD's UI, which will still be executed with Argo CD's control-plane service account.
|
||||
Impersonation is supported for the lifecycle of objects managed by an Application directly, which includes sync operations (creation, update and pruning of resources) and deletion as part of Application finalizer logic. It is also supported for UI operations triggered by the user.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
40
docs/operator-manual/upgrading/3.4-3.5.md
Normal file
40
docs/operator-manual/upgrading/3.4-3.5.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# v3.4 to 3.5
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
## Behavioral Improvements / Fixes
|
||||
|
||||
### Impersonation extended to server operations
|
||||
|
||||
When [impersonation](../app-sync-using-impersonation.md) is enabled, it now applies to all API server operations, not just sync operations. This means that actions triggered through the UI or API (viewing logs, listing events, deleting resources, running resource actions, etc.) will use the impersonated service account derived from the AppProject's `destinationServiceAccounts` configuration.
|
||||
|
||||
Previously, impersonation only applied to sync operations.
|
||||
|
||||
**Affected operations and required permissions:**
|
||||
|
||||
| Operation | Kubernetes API call | Required RBAC verbs |
|
||||
|---|---|---|
|
||||
| Get resource | `GET` on the target resource | `get` |
|
||||
| Patch resource | `PATCH` on the target resource | `get`, `patch` |
|
||||
| Delete resource | `DELETE` on the target resource | `delete` |
|
||||
| List resource events | `LIST` on `events` (core/v1) | `list` |
|
||||
| View pod logs | `GET` on `pods` and `pods/log` | `get` |
|
||||
| Run resource action | `GET`, `CREATE`, `PATCH` on the target resource | `get`, `create`, `patch` |
|
||||
|
||||
This list covers built-in operations. Custom resource actions may require additional permissions depending on what Kubernetes API calls they make.
|
||||
|
||||
Users with impersonation enabled must ensure the service accounts configured in `destinationServiceAccounts` have permissions for these operations.
|
||||
|
||||
No action is required for users who do not have impersonation enabled.
|
||||
|
||||
## API Changes
|
||||
|
||||
## Security Changes
|
||||
|
||||
## Deprecated Items
|
||||
|
||||
## Kustomize Upgraded
|
||||
|
||||
## Helm Upgraded
|
||||
|
||||
## Custom Healthchecks Added
|
||||
@@ -39,6 +39,7 @@ kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubuse
|
||||
|
||||
<hr/>
|
||||
|
||||
- [v3.4 to v3.5](./3.4-3.5.md)
|
||||
- [v3.3 to v3.4](./3.3-3.4.md)
|
||||
- [v3.2 to v3.3](./3.2-3.3.md)
|
||||
- [v3.1 to v3.2](./3.1-3.2.md)
|
||||
|
||||
@@ -136,6 +136,7 @@ nav:
|
||||
- operator-manual/server-commands/additional-configuration-method.md
|
||||
- Upgrading:
|
||||
- operator-manual/upgrading/overview.md
|
||||
- operator-manual/upgrading/3.4-3.5.md
|
||||
- operator-manual/upgrading/3.3-3.4.md
|
||||
- operator-manual/upgrading/3.2-3.3.md
|
||||
- operator-manual/upgrading/3.1-3.2.md
|
||||
|
||||
@@ -508,7 +508,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan
|
||||
return fmt.Errorf("error getting app instance label key from settings: %w", err)
|
||||
}
|
||||
|
||||
config, err := s.getApplicationClusterConfig(ctx, a)
|
||||
config, err := s.getApplicationClusterConfig(ctx, a, proj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting application cluster config: %w", err)
|
||||
}
|
||||
@@ -670,7 +670,7 @@ func (s *Server) GetManifestsWithFiles(stream application.ApplicationService_Get
|
||||
return fmt.Errorf("error getting trackingMethod from settings: %w", err)
|
||||
}
|
||||
|
||||
config, err := s.getApplicationClusterConfig(ctx, a)
|
||||
config, err := s.getApplicationClusterConfig(ctx, a, proj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting application cluster config: %w", err)
|
||||
}
|
||||
@@ -879,7 +879,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*v1a
|
||||
|
||||
// ListResourceEvents returns a list of event resources
|
||||
func (s *Server) ListResourceEvents(ctx context.Context, q *application.ApplicationResourceEventsQuery) (*corev1.EventList, error) {
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, p, err := s.getApplicationEnforceRBACInformer(ctx, rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -918,7 +918,7 @@ func (s *Server) ListResourceEvents(ctx context.Context, q *application.Applicat
|
||||
|
||||
namespace = q.GetResourceNamespace()
|
||||
var config *rest.Config
|
||||
config, err = s.getApplicationClusterConfig(ctx, a)
|
||||
config, err = s.getApplicationClusterConfig(ctx, a, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting application cluster config: %w", err)
|
||||
}
|
||||
@@ -1377,7 +1377,7 @@ func (s *Server) validateAndNormalizeApp(ctx context.Context, app *v1alpha1.Appl
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getApplicationClusterConfig(ctx context.Context, a *v1alpha1.Application) (*rest.Config, error) {
|
||||
func (s *Server) getApplicationClusterConfig(ctx context.Context, a *v1alpha1.Application, p *v1alpha1.AppProject) (*rest.Config, error) {
|
||||
cluster, err := argo.GetDestinationCluster(ctx, a.Spec.Destination, s.db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error validating destination: %w", err)
|
||||
@@ -1387,6 +1387,24 @@ func (s *Server) getApplicationClusterConfig(ctx context.Context, a *v1alpha1.Ap
|
||||
return nil, fmt.Errorf("error getting cluster REST config: %w", err)
|
||||
}
|
||||
|
||||
impersonationEnabled, err := s.settingsMgr.IsImpersonationEnabled()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting impersonation setting: %w", err)
|
||||
}
|
||||
|
||||
if !impersonationEnabled {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
user, err := settings.DeriveServiceAccountToImpersonate(p, a, cluster)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving service account to impersonate: %w", err)
|
||||
}
|
||||
|
||||
config.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: user,
|
||||
}
|
||||
|
||||
return config, err
|
||||
}
|
||||
|
||||
@@ -1437,7 +1455,7 @@ func (s *Server) getAppLiveResource(ctx context.Context, action string, q *appli
|
||||
if fineGrainedInheritanceDisabled && (action == rbac.ActionDelete || action == rbac.ActionUpdate) {
|
||||
action = fmt.Sprintf("%s/%s/%s/%s/%s", action, q.GetGroup(), q.GetKind(), q.GetNamespace(), q.GetResourceName())
|
||||
}
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, p, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if !fineGrainedInheritanceDisabled && err != nil && errors.Is(err, argocommon.PermissionDeniedAPIError) && (action == rbac.ActionDelete || action == rbac.ActionUpdate) {
|
||||
action = fmt.Sprintf("%s/%s/%s/%s/%s", action, q.GetGroup(), q.GetKind(), q.GetNamespace(), q.GetResourceName())
|
||||
a, _, err = s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
@@ -1455,10 +1473,11 @@ func (s *Server) getAppLiveResource(ctx context.Context, action string, q *appli
|
||||
if found == nil || found.UID == "" {
|
||||
return nil, nil, nil, status.Errorf(codes.InvalidArgument, "%s %s %s not found as part of application %s", q.GetKind(), q.GetGroup(), q.GetResourceName(), q.GetName())
|
||||
}
|
||||
config, err := s.getApplicationClusterConfig(ctx, a)
|
||||
config, err := s.getApplicationClusterConfig(ctx, a, p)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("error getting application cluster config: %w", err)
|
||||
}
|
||||
|
||||
return found, config, a, nil
|
||||
}
|
||||
|
||||
@@ -1571,6 +1590,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR
|
||||
propagationPolicy := metav1.DeletePropagationForeground
|
||||
deleteOption = metav1.DeleteOptions{PropagationPolicy: &propagationPolicy}
|
||||
}
|
||||
|
||||
err = s.kubectl.DeleteResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace, deleteOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deleting resource: %w", err)
|
||||
@@ -1826,7 +1846,7 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application.
|
||||
}
|
||||
}
|
||||
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, p, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1840,7 +1860,7 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application.
|
||||
return fmt.Errorf("error getting app resource tree: %w", err)
|
||||
}
|
||||
|
||||
config, err := s.getApplicationClusterConfig(ws.Context(), a)
|
||||
config, err := s.getApplicationClusterConfig(ws.Context(), a, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting application cluster config: %w", err)
|
||||
}
|
||||
@@ -2515,7 +2535,8 @@ func (s *Server) ListResourceActions(ctx context.Context, q *application.Applica
|
||||
|
||||
func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacRequest string, q *application.ApplicationResourceRequest) (obj *unstructured.Unstructured, res *v1alpha1.ResourceNode, app *v1alpha1.Application, config *rest.Config, err error) {
|
||||
if q.GetKind() == applicationType.ApplicationKind && q.GetGroup() == applicationType.Group && q.GetName() == q.GetResourceName() {
|
||||
app, _, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
var p *v1alpha1.AppProject
|
||||
app, p, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
@@ -2523,7 +2544,7 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
config, err = s.getApplicationClusterConfig(ctx, app)
|
||||
config, err = s.getApplicationClusterConfig(ctx, app, p)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("error getting application cluster config: %w", err)
|
||||
}
|
||||
|
||||
@@ -4644,3 +4644,129 @@ func TestTerminateOperationWithConflicts(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, updateCallCount, 2, "Update should be called at least twice (once with conflict, once with success)")
|
||||
}
|
||||
|
||||
func TestGetApplicationClusterConfig(t *testing.T) {
|
||||
t.Run("ImpersonationDisabled", func(t *testing.T) {
|
||||
app := newTestApp()
|
||||
appServer := newTestAppServer(t, app)
|
||||
|
||||
project := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
},
|
||||
}
|
||||
|
||||
config, err := appServer.getApplicationClusterConfig(t.Context(), app, project)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, config.Impersonate.UserName)
|
||||
})
|
||||
|
||||
t.Run("ImpersonationEnabledWithMatch", func(t *testing.T) {
|
||||
f := func(enf *rbac.Enforcer) {
|
||||
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
|
||||
enf.SetDefaultRole("role:admin")
|
||||
}
|
||||
|
||||
projWithSA := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "proj-impersonate", Namespace: "default"},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: test.FakeDestNamespace,
|
||||
DefaultServiceAccount: "test-sa",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := newTestApp(func(a *v1alpha1.Application) {
|
||||
a.Spec.Project = "proj-impersonate"
|
||||
})
|
||||
|
||||
appServer := newTestAppServerWithEnforcerConfigure(t, f,
|
||||
map[string]string{"application.sync.impersonation.enabled": "true"},
|
||||
app, projWithSA,
|
||||
)
|
||||
|
||||
config, err := appServer.getApplicationClusterConfig(t.Context(), app, projWithSA)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "system:serviceaccount:"+test.FakeDestNamespace+":test-sa", config.Impersonate.UserName)
|
||||
})
|
||||
|
||||
t.Run("ImpersonationEnabledWithNoMatch", func(t *testing.T) {
|
||||
f := func(enf *rbac.Enforcer) {
|
||||
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
|
||||
enf.SetDefaultRole("role:admin")
|
||||
}
|
||||
|
||||
app := newTestApp()
|
||||
appServer := newTestAppServerWithEnforcerConfigure(t, f,
|
||||
map[string]string{"application.sync.impersonation.enabled": "true"},
|
||||
app,
|
||||
)
|
||||
|
||||
// "default" project has no DestinationServiceAccounts
|
||||
project := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
},
|
||||
}
|
||||
|
||||
config, err := appServer.getApplicationClusterConfig(t.Context(), app, project)
|
||||
assert.Nil(t, config)
|
||||
assert.ErrorContains(t, err, "no matching service account found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUnstructuredLiveResourceOrAppWithImpersonation(t *testing.T) {
|
||||
f := func(enf *rbac.Enforcer) {
|
||||
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
|
||||
enf.SetDefaultRole("role:admin")
|
||||
}
|
||||
|
||||
projWithSA := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "proj-impersonate", Namespace: "default"},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: test.FakeDestNamespace,
|
||||
DefaultServiceAccount: "test-sa",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := newTestApp(func(a *v1alpha1.Application) {
|
||||
a.Spec.Project = "proj-impersonate"
|
||||
})
|
||||
|
||||
appServer := newTestAppServerWithEnforcerConfigure(t, f,
|
||||
map[string]string{"application.sync.impersonation.enabled": "true"},
|
||||
app, projWithSA,
|
||||
)
|
||||
|
||||
appName := app.Name
|
||||
group := "argoproj.io"
|
||||
kind := "Application"
|
||||
project := "proj-impersonate"
|
||||
|
||||
_, _, _, config, err := appServer.getUnstructuredLiveResourceOrApp(t.Context(), rbac.ActionGet, &application.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
ResourceName: &appName,
|
||||
Group: &group,
|
||||
Kind: &kind,
|
||||
Project: &project,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "system:serviceaccount:"+test.FakeDestNamespace+":test-sa", config.Impersonate.UserName)
|
||||
}
|
||||
|
||||
@@ -334,8 +334,6 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio
|
||||
appsetLister := appFactory.Argoproj().V1alpha1().ApplicationSets().Lister()
|
||||
|
||||
userStateStorage := util_session.NewUserStateStorage(opts.RedisClient)
|
||||
ssoClientApp, err := oidc.NewClientApp(settings, opts.DexServerAddr, opts.DexTLSConfig, opts.BaseHRef, cacheutil.NewRedisCache(opts.RedisClient, settings.UserInfoCacheExpiration(), cacheutil.RedisCompressionNone))
|
||||
errorsutil.CheckError(err)
|
||||
sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.DexTLSConfig, userStateStorage)
|
||||
enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil)
|
||||
enf.EnableEnforce(!opts.DisableAuth)
|
||||
@@ -383,7 +381,6 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio
|
||||
a := &ArgoCDServer{
|
||||
ArgoCDServerOpts: opts,
|
||||
ApplicationSetOpts: appsetOpts,
|
||||
ssoClientApp: ssoClientApp,
|
||||
log: logger,
|
||||
settings: settings,
|
||||
sessionMgr: sessionMgr,
|
||||
@@ -586,6 +583,10 @@ func (server *ArgoCDServer) Run(ctx context.Context, listeners *Listeners) {
|
||||
if server.RedisClient != nil {
|
||||
cacheutil.CollectMetrics(server.RedisClient, metricsServ, server.userStateStorage.GetLockObject())
|
||||
}
|
||||
// OIDC config needs to be refreshed at each server restart
|
||||
ssoClientApp, err := oidc.NewClientApp(server.settings, server.DexServerAddr, server.DexTLSConfig, server.BaseHRef, cacheutil.NewRedisCache(server.RedisClient, server.settings.UserInfoCacheExpiration(), cacheutil.RedisCompressionNone))
|
||||
errorsutil.CheckError(err)
|
||||
server.ssoClientApp = ssoClientApp
|
||||
|
||||
// Don't init storage until after CollectMetrics. CollectMetrics adds hooks to the Redis client, and Init
|
||||
// reads those hooks. If this is called first, there may be a data race.
|
||||
|
||||
@@ -488,6 +488,100 @@ func TestGracefulShutdown(t *testing.T) {
|
||||
assert.True(t, shutdown)
|
||||
}
|
||||
|
||||
func TestOIDCRefresh(t *testing.T) {
|
||||
port, err := test.GetFreePort()
|
||||
require.NoError(t, err)
|
||||
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
|
||||
cm := test.NewFakeConfigMap()
|
||||
cm.Data["oidc.config"] = `
|
||||
name: Test OIDC
|
||||
issuer: $oidc.myoidc.issuer
|
||||
clientID: $oidc.myoidc.clientId
|
||||
clientSecret: $oidc.myoidc.clientSecret
|
||||
`
|
||||
secret := test.NewFakeSecret()
|
||||
issuerURL := "http://oidc.127.0.0.1.nip.io"
|
||||
updatedIssuerURL := "http://newoidc.127.0.0.1.nip.io"
|
||||
secret.Data["oidc.myoidc.issuer"] = []byte(issuerURL)
|
||||
secret.Data["oidc.myoidc.clientId"] = []byte("myClientId")
|
||||
secret.Data["oidc.myoidc.clientSecret"] = []byte("myClientSecret")
|
||||
|
||||
kubeclientset := fake.NewSimpleClientset(cm, secret)
|
||||
redis, redisCloser := test.NewInMemoryRedis()
|
||||
defer redisCloser()
|
||||
s := NewServer(
|
||||
t.Context(),
|
||||
ArgoCDServerOpts{
|
||||
ListenPort: port,
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: apps.NewSimpleClientset(),
|
||||
RepoClientset: mockRepoClient,
|
||||
RedisClient: redis,
|
||||
},
|
||||
ApplicationSetOpts{},
|
||||
)
|
||||
projInformerCancel := test.StartInformer(s.projInformer)
|
||||
defer projInformerCancel()
|
||||
appInformerCancel := test.StartInformer(s.appInformer)
|
||||
defer appInformerCancel()
|
||||
appsetInformerCancel := test.StartInformer(s.appsetInformer)
|
||||
defer appsetInformerCancel()
|
||||
clusterInformerCancel := test.StartInformer(s.clusterInformer)
|
||||
defer clusterInformerCancel()
|
||||
|
||||
shutdown := false
|
||||
|
||||
lns, err := s.Listen()
|
||||
require.NoError(t, err)
|
||||
runCtx := t.Context()
|
||||
|
||||
var wg gosync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func(shutdown *bool) {
|
||||
defer wg.Done()
|
||||
s.Run(runCtx, lns)
|
||||
*shutdown = true
|
||||
}(&shutdown)
|
||||
|
||||
for !s.available.Load() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
assert.True(t, s.available.Load())
|
||||
assert.Equal(t, issuerURL, s.ssoClientApp.IssuerURL())
|
||||
|
||||
// Update oidc config
|
||||
secret.Data["oidc.myoidc.issuer"] = []byte(updatedIssuerURL)
|
||||
secret.ResourceVersion = "12345"
|
||||
_, err = kubeclientset.CoreV1().Secrets(test.FakeArgoCDNamespace).Update(runCtx, secret, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for graceful shutdown
|
||||
wg.Wait()
|
||||
for s.available.Load() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
assert.False(t, s.available.Load())
|
||||
|
||||
shutdown = false
|
||||
wg.Add(1)
|
||||
go func(shutdown *bool) {
|
||||
defer wg.Done()
|
||||
s.Run(runCtx, lns)
|
||||
*shutdown = true
|
||||
}(&shutdown)
|
||||
|
||||
for !s.available.Load() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
assert.True(t, s.available.Load())
|
||||
assert.Equal(t, updatedIssuerURL, s.ssoClientApp.IssuerURL())
|
||||
|
||||
s.stopCh <- syscall.SIGINT
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
type testData struct {
|
||||
test string
|
||||
|
||||
@@ -14,7 +14,7 @@ FROM docker.io/library/registry:3.0@sha256:6c5666b861f3505b116bb9aa9b25175e71210
|
||||
|
||||
FROM docker.io/bitnamilegacy/kubectl:1.32@sha256:9524faf8e3cefb47fa28244a5d15f95ec21a73d963273798e593e61f80712333 AS kubectl
|
||||
|
||||
FROM docker.io/library/ubuntu:26.04@sha256:91832dcd7bc5e44c098ecefc0a251a5c5d596dae494b33fb248e01b6840f8ce0
|
||||
FROM docker.io/library/ubuntu:26.04@sha256:730382b4a53a3c4a1498b7a36f11a62117f133fe6e73b01bb91303ed2ad87cdd
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestSimpleGitDirectoryGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestSimpleGitDirectoryGeneratorGoTemplate(t *testing.T) {
|
||||
@@ -240,7 +240,7 @@ func TestSimpleGitDirectoryGeneratorGoTemplate(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
||||
@@ -335,7 +335,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
||||
// verify the ApplicationSet error status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
||||
@@ -438,7 +438,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitFilesGenerator(t *testing.T) {
|
||||
@@ -544,7 +544,7 @@ func TestSimpleGitFilesGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
||||
@@ -639,7 +639,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
|
||||
// verify the ApplicationSet error status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
||||
@@ -738,7 +738,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
|
||||
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
|
||||
Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitFilesGeneratorGoTemplate(t *testing.T) {
|
||||
@@ -845,7 +845,7 @@ func TestSimpleGitFilesGeneratorGoTemplate(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestSimpleGitFilesPreserveResourcesOnDeletion(t *testing.T) {
|
||||
@@ -894,7 +894,7 @@ func TestSimpleGitFilesPreserveResourcesOnDeletion(t *testing.T) {
|
||||
// We use an extra-long duration here, as we might need to wait for image pull.
|
||||
}).Then().ExpectWithDuration(Pod(t, func(p corev1.Pod) bool { return strings.Contains(p.Name, "guestbook-ui") }), 6*time.Minute).
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationForeground).
|
||||
And(func() {
|
||||
t.Log("Waiting 15 seconds to give the cluster a chance to delete the pods.")
|
||||
// Wait 15 seconds to give the cluster a chance to deletes the pods, if it is going to do so.
|
||||
@@ -952,7 +952,7 @@ func TestSimpleGitFilesPreserveResourcesOnDeletionGoTemplate(t *testing.T) {
|
||||
// We use an extra-long duration here, as we might need to wait for image pull.
|
||||
}).Then().ExpectWithDuration(Pod(t, func(p corev1.Pod) bool { return strings.Contains(p.Name, "guestbook-ui") }), 6*time.Minute).
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationForeground).
|
||||
And(func() {
|
||||
t.Log("Waiting 15 seconds to give the cluster a chance to delete the pods.")
|
||||
// Wait 15 seconds to give the cluster a chance to deletes the pods, if it is going to do so.
|
||||
@@ -1034,7 +1034,7 @@ func TestGitGeneratorPrivateRepo(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestGitGeneratorPrivateRepoGoTemplate(t *testing.T) {
|
||||
@@ -1108,7 +1108,7 @@ func TestGitGeneratorPrivateRepoGoTemplate(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestSimpleGitGeneratorPrivateRepoWithNoRepo(t *testing.T) {
|
||||
@@ -1180,7 +1180,7 @@ func TestSimpleGitGeneratorPrivateRepoWithNoRepo(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestSimpleGitGeneratorPrivateRepoWithMatchingProject(t *testing.T) {
|
||||
@@ -1251,7 +1251,7 @@ func TestSimpleGitGeneratorPrivateRepoWithMatchingProject(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestSimpleGitGeneratorPrivateRepoWithMismatchingProject(t *testing.T) {
|
||||
@@ -1324,7 +1324,7 @@ func TestSimpleGitGeneratorPrivateRepoWithMismatchingProject(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestGitGeneratorPrivateRepoWithTemplatedProject(t *testing.T) {
|
||||
@@ -1400,7 +1400,7 @@ func TestGitGeneratorPrivateRepoWithTemplatedProject(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestGitGeneratorPrivateRepoWithTemplatedProjectAndProjectScopedRepo(t *testing.T) {
|
||||
@@ -1484,5 +1484,5 @@ func TestGitGeneratorPrivateRepoWithTemplatedProjectAndProjectScopedRepo(t *test
|
||||
}).Then().Expect(ApplicationsDoNotExist(expectedApps)).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ func TestApplicationSetProgressiveSyncStep(t *testing.T) {
|
||||
ExpectWithDuration(CheckApplicationInRightSteps("3", []string{"app3-prod"}), time.Second*5).
|
||||
// cleanup
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationForeground).
|
||||
Then().
|
||||
ExpectWithDuration(ApplicationsDoNotExist([]v1alpha1.Application{expectedDevApp, expectedStageApp, expectedProdApp}), time.Minute)
|
||||
}
|
||||
@@ -184,9 +184,9 @@ func TestProgressiveSyncHealthGating(t *testing.T) {
|
||||
if os.Getenv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS") != "true" {
|
||||
t.Skip("Skipping progressive sync tests - ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS not enabled")
|
||||
}
|
||||
expectedDevApp := generateExpectedApp("prog-", "progressive-sync/", "dev", "dev")
|
||||
expectedStageApp := generateExpectedApp("prog-", "progressive-sync/", "staging", "staging")
|
||||
expectedProdApp := generateExpectedApp("prog-", "progressive-sync/", "prod", "prod")
|
||||
expectedDevApp := generateExpectedApp("prog-", "progressive-sync/", "dev", "dev", "")
|
||||
expectedStageApp := generateExpectedApp("prog-", "progressive-sync/", "staging", "staging", "")
|
||||
expectedProdApp := generateExpectedApp("prog-", "progressive-sync/", "prod", "prod", "")
|
||||
|
||||
expectedStatusWave1 := map[string]v1alpha1.ApplicationSetApplicationStatus{
|
||||
"prog-dev": {
|
||||
@@ -343,7 +343,7 @@ func TestProgressiveSyncHealthGating(t *testing.T) {
|
||||
}).
|
||||
// Cleanup
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationForeground).
|
||||
Then().
|
||||
ExpectWithDuration(ApplicationsDoNotExist([]v1alpha1.Application{expectedDevApp, expectedStageApp, expectedProdApp}), TransitionTimeout)
|
||||
}
|
||||
@@ -381,9 +381,9 @@ func TestNoApplicationStatusWhenNoSteps(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedApps := []v1alpha1.Application{
|
||||
generateExpectedApp("prog-", "progressive-sync/", "dev", "dev"),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "staging", "staging"),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "prod", "prod"),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "dev", "dev", ""),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "staging", "staging", ""),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "prod", "prod", ""),
|
||||
}
|
||||
Given(t).
|
||||
When().
|
||||
@@ -393,7 +393,7 @@ func TestNoApplicationStatusWhenNoSteps(t *testing.T) {
|
||||
Expect(ApplicationSetDoesNotHaveApplicationStatus()).
|
||||
// Cleanup
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationForeground).
|
||||
Then().
|
||||
ExpectWithDuration(ApplicationsDoNotExist(expectedApps), TransitionTimeout)
|
||||
}
|
||||
@@ -403,9 +403,9 @@ func TestNoApplicationStatusWhenNoApplications(t *testing.T) {
|
||||
t.Skip("Skipping progressive sync tests - ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS not enabled")
|
||||
}
|
||||
expectedApps := []v1alpha1.Application{
|
||||
generateExpectedApp("prog-", "progressive-sync/", "dev", "dev"),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "staging", "staging"),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "prod", "prod"),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "dev", "dev", ""),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "staging", "staging", ""),
|
||||
generateExpectedApp("prog-", "progressive-sync/", "prod", "prod", ""),
|
||||
}
|
||||
Given(t).
|
||||
When().
|
||||
@@ -415,37 +415,107 @@ func TestNoApplicationStatusWhenNoApplications(t *testing.T) {
|
||||
Expect(ApplicationSetDoesNotHaveApplicationStatus()).
|
||||
// Cleanup
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationForeground).
|
||||
Then().
|
||||
Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestProgressiveSyncMultipleAppsPerStep(t *testing.T) {
|
||||
func TestProgressiveSyncMultipleAppsPerStepWithReverseDeletionOrder(t *testing.T) {
|
||||
if os.Getenv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS") != "true" {
|
||||
t.Skip("Skipping progressive sync tests - ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS not enabled")
|
||||
}
|
||||
expectedApps := []v1alpha1.Application{
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/dev/", "sketch", "dev"),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/dev/", "build", "dev"),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/staging/", "verify", "staging"),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/staging/", "validate", "staging"),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/prod/", "ship", "prod"),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/prod/", "run", "prod"),
|
||||
// Define app groups by step (for reverse deletion: prod -> staging -> dev)
|
||||
prodApps := []string{"prog-ship", "prog-run"}
|
||||
stagingApps := []string{"prog-verify", "prog-validate"}
|
||||
devApps := []string{"prog-sketch", "prog-build"}
|
||||
testFinalizer := "test.e2e.argoproj.io/wait-for-verification"
|
||||
// Create expected app definitions for existence checks
|
||||
expectedProdApps := []v1alpha1.Application{
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/prod/", "ship", "prod", testFinalizer),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/prod/", "run", "prod", testFinalizer),
|
||||
}
|
||||
expectedStagingApps := []v1alpha1.Application{
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/staging/", "verify", "staging", testFinalizer),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/staging/", "validate", "staging", testFinalizer),
|
||||
}
|
||||
expectedDevApps := []v1alpha1.Application{
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/dev/", "sketch", "dev", testFinalizer),
|
||||
generateExpectedApp("prog-", "progressive-sync/multiple-apps-in-step/dev/", "build", "dev", testFinalizer),
|
||||
}
|
||||
var allExpectedApps []v1alpha1.Application
|
||||
allExpectedApps = append(allExpectedApps, expectedProdApps...)
|
||||
allExpectedApps = append(allExpectedApps, expectedStagingApps...)
|
||||
allExpectedApps = append(allExpectedApps, expectedDevApps...)
|
||||
|
||||
Given(t).
|
||||
When().
|
||||
Create(appSetWithMultipleAppsInEachStep).
|
||||
Create(appSetWithReverseDeletionOrder).
|
||||
Then().
|
||||
Expect(ApplicationsExist(expectedApps)).
|
||||
And(func() {
|
||||
t.Log("ApplicationSet with reverse deletion order created")
|
||||
}).
|
||||
Expect(ApplicationsExist(allExpectedApps)).
|
||||
Expect(CheckApplicationInRightSteps("1", []string{"prog-sketch", "prog-build"})).
|
||||
Expect(CheckApplicationInRightSteps("2", []string{"prog-verify", "prog-validate"})).
|
||||
Expect(CheckApplicationInRightSteps("3", []string{"prog-ship", "prog-run"})).
|
||||
ExpectWithDuration(ApplicationSetHasApplicationStatus(6), TransitionTimeout).
|
||||
// Cleanup
|
||||
And(func() {
|
||||
t.Log("All 6 applications exist and are tracked in ApplicationSet status")
|
||||
}).
|
||||
// Delete the ApplicationSet
|
||||
When().
|
||||
Delete().
|
||||
Delete(metav1.DeletePropagationBackground).
|
||||
Then().
|
||||
Expect(ApplicationsDoNotExist(expectedApps))
|
||||
And(func() {
|
||||
t.Log("Starting deletion - should happen in reverse order: prod -> staging -> dev")
|
||||
t.Log("Wave 1: Verifying prod apps (prog-ship, prog-run) are deleted first")
|
||||
}).
|
||||
// Wave 1: Prod apps should be deleted first, others untouched
|
||||
Expect(ApplicationDeletionStarted(prodApps)).
|
||||
Expect(ApplicationsExistAndNotBeingDeleted(append(stagingApps, devApps...))).
|
||||
And(func() {
|
||||
t.Log("Wave 1 confirmed: prod apps deleting/gone, staging and dev apps still exist and not being deleted")
|
||||
}).
|
||||
When().
|
||||
RemoveFinalizerFromApps(prodApps, testFinalizer).
|
||||
Then().
|
||||
And(func() {
|
||||
t.Log("removed finalizer from prod apps, confirm prod apps deleted")
|
||||
t.Log("Wave 2: Verifying staging apps (prog-verify, prog-validate) are deleted second")
|
||||
}).
|
||||
// Wave 2: Staging apps being deleted, dev untouched
|
||||
ExpectWithDuration(ApplicationsDoNotExist(expectedProdApps), TransitionTimeout).
|
||||
Expect(ApplicationDeletionStarted(stagingApps)).
|
||||
Expect(ApplicationsExistAndNotBeingDeleted(devApps)).
|
||||
And(func() {
|
||||
t.Log("Wave 2 confirmed: prod apps gone, staging apps deleting/gone, dev apps still exist and not being deleted")
|
||||
}).
|
||||
When().
|
||||
RemoveFinalizerFromApps(stagingApps, testFinalizer).
|
||||
Then().
|
||||
And(func() {
|
||||
t.Log("removed finalizer from staging apps, confirm staging apps deleted")
|
||||
t.Log("Wave 3: Verifying dev apps (prog-sketch, prog-build) are deleted last")
|
||||
}).
|
||||
// Wave 3: Dev apps deleted last
|
||||
ExpectWithDuration(ApplicationsDoNotExist(expectedStagingApps), TransitionTimeout).
|
||||
Expect(ApplicationDeletionStarted(devApps)).
|
||||
And(func() {
|
||||
t.Log("Wave 3 confirmed: all prod and staging apps gone, dev apps deleting/gone")
|
||||
}).
|
||||
When().
|
||||
RemoveFinalizerFromApps(devApps, testFinalizer).
|
||||
Then().
|
||||
And(func() {
|
||||
t.Log("removed finalizer from dev apps, confirm dev apps deleted")
|
||||
t.Log("Waiting for final cleanup - all applications should be deleted")
|
||||
}).
|
||||
// Final: All applications should be gone
|
||||
ExpectWithDuration(ApplicationsDoNotExist(allExpectedApps), time.Minute).
|
||||
And(func() {
|
||||
t.Log("Reverse deletion order verified successfully!")
|
||||
t.Log("Deletion sequence was: prod -> staging -> dev")
|
||||
})
|
||||
}
|
||||
|
||||
var appSetInvalidStepConfiguration = v1alpha1.ApplicationSet{
|
||||
@@ -556,9 +626,13 @@ var appSetWithEmptyGenerator = v1alpha1.ApplicationSet{
|
||||
},
|
||||
}
|
||||
|
||||
var appSetWithMultipleAppsInEachStep = v1alpha1.ApplicationSet{
|
||||
var appSetWithReverseDeletionOrder = v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "progressive-sync-multi-apps",
|
||||
Name: "appset-reverse-deletion-order",
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ApplicationSet",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
GoTemplate: true,
|
||||
@@ -569,6 +643,10 @@ var appSetWithMultipleAppsInEachStep = v1alpha1.ApplicationSet{
|
||||
Labels: map[string]string{
|
||||
"environment": "{{.environment}}",
|
||||
},
|
||||
Finalizers: []string{
|
||||
"resources-finalizer.argocd.argoproj.io",
|
||||
"test.e2e.argoproj.io/wait-for-verification",
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
@@ -605,11 +683,18 @@ var appSetWithMultipleAppsInEachStep = v1alpha1.ApplicationSet{
|
||||
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
|
||||
Steps: generateStandardRolloutSyncSteps(),
|
||||
},
|
||||
DeletionOrder: "Reverse",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func generateExpectedApp(prefix string, path string, name string, envVar string) v1alpha1.Application {
|
||||
func generateExpectedApp(prefix string, path string, name string, envVar string, testFinalizer string) v1alpha1.Application {
|
||||
finalizers := []string{
|
||||
"resources-finalizer.argocd.argoproj.io",
|
||||
}
|
||||
if testFinalizer != "" {
|
||||
finalizers = append(finalizers, testFinalizer)
|
||||
}
|
||||
return v1alpha1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
@@ -621,9 +706,7 @@ func generateExpectedApp(prefix string, path string, name string, envVar string)
|
||||
Labels: map[string]string{
|
||||
"environment": envVar,
|
||||
},
|
||||
Finalizers: []string{
|
||||
"resources-finalizer.argocd.argoproj.io",
|
||||
},
|
||||
Finalizers: finalizers,
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestSimpleListGeneratorExternalNamespace(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
}
|
||||
|
||||
func TestSimpleListGeneratorExternalNamespaceNoConflict(t *testing.T) {
|
||||
@@ -325,13 +325,13 @@ func TestSimpleListGeneratorExternalNamespaceNoConflict(t *testing.T) {
|
||||
Then().
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata})).
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata})).
|
||||
When().
|
||||
SwitchToExternalNamespace(utils.ArgoCDExternalNamespace2).
|
||||
Then().
|
||||
Expect(ApplicationsExist([]v1alpha1.Application{expectedAppExternalNamespace2})).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppExternalNamespace2}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppExternalNamespace2}))
|
||||
}
|
||||
|
||||
func TestSimpleListGenerator(t *testing.T) {
|
||||
@@ -420,7 +420,7 @@ func TestSimpleListGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
}
|
||||
|
||||
func TestSimpleListGeneratorGoTemplate(t *testing.T) {
|
||||
@@ -509,7 +509,7 @@ func TestSimpleListGeneratorGoTemplate(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
}
|
||||
|
||||
func TestRenderHelmValuesObject(t *testing.T) {
|
||||
@@ -581,7 +581,7 @@ func TestRenderHelmValuesObject(t *testing.T) {
|
||||
}).Then().Expect(ApplicationsExist([]v1alpha1.Application{expectedApp})).
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
}
|
||||
|
||||
func TestTemplatePatch(t *testing.T) {
|
||||
@@ -705,7 +705,7 @@ func TestTemplatePatch(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
}
|
||||
|
||||
func TestUpdateHelmValuesObject(t *testing.T) {
|
||||
@@ -787,7 +787,7 @@ func TestUpdateHelmValuesObject(t *testing.T) {
|
||||
Expect(ApplicationsExist([]v1alpha1.Application{expectedApp})).
|
||||
When().
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
}
|
||||
|
||||
func TestSyncPolicyCreateUpdate(t *testing.T) {
|
||||
@@ -898,7 +898,7 @@ func TestSyncPolicyCreateUpdate(t *testing.T) {
|
||||
// As policy is create-update, AppSet controller will remove all generated applications's ownerReferences on delete AppSet
|
||||
// So AppSet deletion will be reflected, but all the applications it generates will still exist
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsExist([]v1alpha1.Application{*expectedAppNewMetadata}))
|
||||
}
|
||||
|
||||
func TestSyncPolicyCreateDelete(t *testing.T) {
|
||||
@@ -994,7 +994,7 @@ func TestSyncPolicyCreateDelete(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
}
|
||||
|
||||
func TestSyncPolicyCreateOnly(t *testing.T) {
|
||||
@@ -1095,7 +1095,7 @@ func TestSyncPolicyCreateOnly(t *testing.T) {
|
||||
// As policy is create-update, AppSet controller will remove all generated applications's ownerReferences on delete AppSet
|
||||
// So AppSet deletion will be reflected, but all the applications it generates will still exist
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
}
|
||||
|
||||
func githubSCMMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
|
||||
@@ -1582,7 +1582,7 @@ func TestCustomApplicationFinalizers(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
}
|
||||
|
||||
func TestCustomApplicationFinalizersGoTemplate(t *testing.T) {
|
||||
@@ -1647,7 +1647,7 @@ func TestCustomApplicationFinalizersGoTemplate(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
}
|
||||
|
||||
func githubPullMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
|
||||
@@ -2174,7 +2174,7 @@ func TestApplicationSetAPIListResourceEvents(t *testing.T) {
|
||||
// Events list should be returned (may be empty if no events have been generated yet)
|
||||
assert.NotNil(t, events)
|
||||
}).
|
||||
When().Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{}))
|
||||
When().Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{}))
|
||||
}
|
||||
|
||||
// TestApplicationSetHealthStatusCLI tests that the CLI commands display the health status field for an ApplicationSet.
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestSimpleClusterGeneratorExternalNamespace(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
}
|
||||
|
||||
func TestSimpleClusterGenerator(t *testing.T) {
|
||||
@@ -199,7 +199,7 @@ func TestSimpleClusterGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
}
|
||||
|
||||
func TestClusterGeneratorWithLocalCluster(t *testing.T) {
|
||||
@@ -311,7 +311,7 @@ func TestClusterGeneratorWithLocalCluster(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func TestSimpleClusterGeneratorAddingCluster(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1, expectedAppCluster2}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1, expectedAppCluster2}))
|
||||
}
|
||||
|
||||
func TestSimpleClusterGeneratorDeletingCluster(t *testing.T) {
|
||||
@@ -473,7 +473,7 @@ func TestSimpleClusterGeneratorDeletingCluster(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1}))
|
||||
}
|
||||
|
||||
func TestClusterGeneratorWithFlatListMode(t *testing.T) {
|
||||
@@ -574,5 +574,5 @@ func TestClusterGeneratorWithFlatListMode(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster2}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster2}))
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestSimpleClusterDecisionResourceGeneratorExternalNamespace(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
}
|
||||
|
||||
func TestSimpleClusterDecisionResourceGenerator(t *testing.T) {
|
||||
@@ -218,7 +218,7 @@ func TestSimpleClusterDecisionResourceGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{*expectedAppNewNamespace}))
|
||||
}
|
||||
|
||||
func TestSimpleClusterDecisionResourceGeneratorAddingCluster(t *testing.T) {
|
||||
@@ -310,7 +310,7 @@ func TestSimpleClusterDecisionResourceGeneratorAddingCluster(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1, expectedAppCluster2}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1, expectedAppCluster2}))
|
||||
}
|
||||
|
||||
func TestSimpleClusterDecisionResourceGeneratorDeletingClusterSecret(t *testing.T) {
|
||||
@@ -404,7 +404,7 @@ func TestSimpleClusterDecisionResourceGeneratorDeletingClusterSecret(t *testing.
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1}))
|
||||
}
|
||||
|
||||
func TestSimpleClusterDecisionResourceGeneratorDeletingClusterFromResource(t *testing.T) {
|
||||
@@ -505,5 +505,5 @@ func TestSimpleClusterDecisionResourceGeneratorDeletingClusterFromResource(t *te
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1}))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedAppCluster1}))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/test/e2e/fixture"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -391,7 +394,7 @@ func (a *Actions) StatusUpdatePlacementDecision(placementDecisionName string, cl
|
||||
}
|
||||
|
||||
// Delete deletes the ApplicationSet within the context
|
||||
func (a *Actions) Delete() *Actions {
|
||||
func (a *Actions) Delete(propagationPolicy metav1.DeletionPropagation) *Actions {
|
||||
a.context.T().Helper()
|
||||
|
||||
fixtureClient := utils.GetE2EFixtureK8sClient(a.context.T())
|
||||
@@ -408,9 +411,7 @@ func (a *Actions) Delete() *Actions {
|
||||
} else {
|
||||
appSetClientSet = fixtureClient.AppSetClientset
|
||||
}
|
||||
|
||||
deleteProp := metav1.DeletePropagationForeground
|
||||
err := appSetClientSet.Delete(context.Background(), a.context.GetName(), metav1.DeleteOptions{PropagationPolicy: &deleteProp})
|
||||
err := appSetClientSet.Delete(context.Background(), a.context.GetName(), metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
|
||||
a.describeAction = fmt.Sprintf("Deleting ApplicationSet '%s/%s' %v", a.context.namespace, a.context.GetName(), err)
|
||||
a.lastOutput, a.lastError = "", err
|
||||
a.verifyAction()
|
||||
@@ -566,3 +567,48 @@ func (a *Actions) AddSignedFile(fileName, fileContents string) *Actions {
|
||||
fixture.AddSignedFile(a.context.T(), a.context.path+"/"+fileName, fileContents)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Actions) RemoveFinalizerFromApps(appNames []string, finalizer string) *Actions {
|
||||
a.context.T().Helper()
|
||||
fixtureClient := utils.GetE2EFixtureK8sClient(a.context.T())
|
||||
|
||||
var namespace string
|
||||
if a.context.switchToNamespace != "" {
|
||||
namespace = string(a.context.switchToNamespace)
|
||||
} else {
|
||||
namespace = fixture.TestNamespace()
|
||||
}
|
||||
for _, appName := range appNames {
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
app, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(namespace).Get(
|
||||
a.context.T().Context(), appName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove provided finalizer
|
||||
finalizers := []string{}
|
||||
for _, f := range app.Finalizers {
|
||||
if f != finalizer {
|
||||
finalizers = append(finalizers, f)
|
||||
}
|
||||
}
|
||||
patch, _ := json.Marshal(map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"finalizers": finalizers,
|
||||
},
|
||||
})
|
||||
_, err = fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(namespace).Patch(
|
||||
a.context.T().Context(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
a.lastError = err
|
||||
}
|
||||
}
|
||||
a.describeAction = fmt.Sprintf("removing finalizer '%s' from apps %v", finalizer, appNames)
|
||||
a.verifyAction()
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -336,3 +336,41 @@ func ApplicationSetHasApplicationStatus(expectedApplicationStatusLength int) Exp
|
||||
return succeeded, fmt.Sprintf("All Applications in ApplicationSet: '%s' are Healthy ", c.context.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// ApplicationDeletionStarted verifies at least one application from provided list of appNames has DeletionTimestamp set,
|
||||
// indicating deletion has begun for this step. Returns failed if any application doesn't exist, does not expect completion of deletion.
|
||||
func ApplicationDeletionStarted(appNames []string) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
anyapp := false
|
||||
for _, appName := range appNames {
|
||||
app := c.app(appName)
|
||||
if app == nil {
|
||||
// with test finalizer explicitly added, application should not be deleted
|
||||
return failed, fmt.Sprintf("no application found with name '%s'", c.context.GetName())
|
||||
}
|
||||
if app.DeletionTimestamp != nil {
|
||||
anyapp = true
|
||||
}
|
||||
}
|
||||
if !anyapp {
|
||||
return pending, "no app in this step is being deleted yet"
|
||||
}
|
||||
return succeeded, fmt.Sprintf("at least one app in %v is being deleted or gone", appNames)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplicationsExistAndNotBeingDeleted checks that specified apps exist and do NOT have DeletionTimestamp set
|
||||
func ApplicationsExistAndNotBeingDeleted(appNames []string) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
for _, appName := range appNames {
|
||||
app := c.app(appName)
|
||||
if app == nil {
|
||||
return failed, fmt.Sprintf("app '%s' does not exist but should", appName)
|
||||
}
|
||||
if app.DeletionTimestamp != nil {
|
||||
return failed, fmt.Sprintf("app '%s' is being deleted but should not be yet", appName)
|
||||
}
|
||||
}
|
||||
return succeeded, fmt.Sprintf("all apps %v exist and are not being deleted", appNames)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestListMatrixGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestClusterMatrixGenerator(t *testing.T) {
|
||||
@@ -254,7 +254,7 @@ func TestClusterMatrixGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestMatrixTerminalMatrixGeneratorSelector(t *testing.T) {
|
||||
@@ -392,7 +392,7 @@ func TestMatrixTerminalMatrixGeneratorSelector(t *testing.T) {
|
||||
})
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).Expect(ApplicationsDoNotExist(excludedApps)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(excludedApps)).Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(excludedApps)).Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func TestMatrixTerminalMergeGeneratorSelector(t *testing.T) {
|
||||
@@ -530,5 +530,5 @@ func TestMatrixTerminalMergeGeneratorSelector(t *testing.T) {
|
||||
})
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).Expect(ApplicationsDoNotExist(excludedApps)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(excludedApps)).Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(excludedApps)).Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestListMergeGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestClusterMergeGenerator(t *testing.T) {
|
||||
@@ -271,7 +271,7 @@ func TestClusterMergeGenerator(t *testing.T) {
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
|
||||
}
|
||||
|
||||
func TestMergeTerminalMergeGeneratorSelector(t *testing.T) {
|
||||
@@ -410,7 +410,7 @@ func TestMergeTerminalMergeGeneratorSelector(t *testing.T) {
|
||||
})
|
||||
}).Then().Expect(ApplicationsExist(expectedApps)).Expect(ApplicationsDoNotExist(excludedApps)).
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist(excludedApps)).Expect(ApplicationsDoNotExist(expectedApps))
|
||||
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(excludedApps)).Expect(ApplicationsDoNotExist(expectedApps))
|
||||
}
|
||||
|
||||
func toAPIExtensionsJSON(t *testing.T, g any) *apiextensionsv1.JSON {
|
||||
|
||||
@@ -1074,39 +1074,51 @@ type ClusterGetter interface {
|
||||
GetClusterServersByName(ctx context.Context, server string) ([]string, error)
|
||||
}
|
||||
|
||||
// GetDestinationServer resolves the cluster server URL for the given destination without
|
||||
// fetching the full Cluster object. For server based destinations the URL is returned
|
||||
// directly (normalized). For name based destinations GetClusterServersByName is called.
|
||||
// An error is returned if the name is ambiguous or missing.
|
||||
func GetDestinationServer(ctx context.Context, destination argoappv1.ApplicationDestination, db ClusterGetter) (string, error) {
|
||||
if destination.Name != "" && destination.Server != "" {
|
||||
return "", fmt.Errorf("application destination can't have both name and server defined: %s %s", destination.Name, destination.Server)
|
||||
}
|
||||
if destination.Server != "" {
|
||||
return strings.TrimRight(destination.Server, "/"), nil
|
||||
}
|
||||
if destination.Name != "" {
|
||||
clusterURLs, err := db.GetClusterServersByName(ctx, destination.Name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting cluster by name %q: %w", destination.Name, err)
|
||||
}
|
||||
if len(clusterURLs) == 0 {
|
||||
return "", fmt.Errorf("there are no clusters with this name: %s", destination.Name)
|
||||
}
|
||||
if len(clusterURLs) > 1 {
|
||||
return "", fmt.Errorf("there are %d clusters with the same name: [%s]", len(clusterURLs), strings.Join(clusterURLs, " "))
|
||||
}
|
||||
return clusterURLs[0], nil
|
||||
}
|
||||
// nolint:staticcheck // Error constant is very old, shouldn't lowercase the first letter.
|
||||
return "", errors.New(ErrDestinationMissing)
|
||||
}
|
||||
|
||||
// GetDestinationCluster returns the cluster object based on the destination server or name. If both are provided or
|
||||
// both are empty, an error is returned. If the destination server is provided, the cluster is fetched by the server
|
||||
// URL. If the destination name is provided, the cluster is fetched by the name. If multiple clusters have the specified
|
||||
// name, an error is returned.
|
||||
func GetDestinationCluster(ctx context.Context, destination argoappv1.ApplicationDestination, db ClusterGetter) (*argoappv1.Cluster, error) {
|
||||
if destination.Name != "" && destination.Server != "" {
|
||||
return nil, fmt.Errorf("application destination can't have both name and server defined: %s %s", destination.Name, destination.Server)
|
||||
server, err := GetDestinationServer(ctx, destination, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if destination.Server != "" {
|
||||
cluster, err := db.GetCluster(ctx, destination.Server)
|
||||
if err != nil {
|
||||
cluster, err := db.GetCluster(ctx, server)
|
||||
if err != nil {
|
||||
if destination.Server != "" {
|
||||
return nil, fmt.Errorf("error getting cluster by server %q: %w", destination.Server, err)
|
||||
}
|
||||
return cluster, nil
|
||||
} else if destination.Name != "" {
|
||||
clusterURLs, err := db.GetClusterServersByName(ctx, destination.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting cluster by name %q: %w", destination.Name, err)
|
||||
}
|
||||
if len(clusterURLs) == 0 {
|
||||
return nil, fmt.Errorf("there are no clusters with this name: %s", destination.Name)
|
||||
}
|
||||
if len(clusterURLs) > 1 {
|
||||
return nil, fmt.Errorf("there are %d clusters with the same name: [%s]", len(clusterURLs), strings.Join(clusterURLs, " "))
|
||||
}
|
||||
cluster, err := db.GetCluster(ctx, clusterURLs[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting cluster by URL: %w", err)
|
||||
}
|
||||
return cluster, nil
|
||||
return nil, fmt.Errorf("error getting cluster by URL: %w", err)
|
||||
}
|
||||
// nolint:staticcheck // Error constant is very old, shouldn't lowercase the first letter.
|
||||
return nil, errors.New(ErrDestinationMissing)
|
||||
return cluster, nil
|
||||
}
|
||||
|
||||
func GetGlobalProjects(proj *argoappv1.AppProject, projLister applicationsv1.AppProjectLister, settingsManager *settings.SettingsManager) []*argoappv1.AppProject {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -30,7 +31,7 @@ const (
|
||||
|
||||
var (
|
||||
localCluster = appv1.Cluster{
|
||||
Name: "in-cluster",
|
||||
Name: appv1.KubernetesInClusterName,
|
||||
Server: appv1.KubernetesInternalAPIServerAddr,
|
||||
Info: appv1.ClusterInfo{
|
||||
ConnectionState: appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful},
|
||||
@@ -233,7 +234,10 @@ func (db *db) getClusterSecret(server string) (*corev1.Secret, error) {
|
||||
|
||||
// GetCluster returns a cluster from a query
|
||||
func (db *db) GetCluster(_ context.Context, server string) (*appv1.Cluster, error) {
|
||||
informer := db.settingsMgr.GetClusterInformer()
|
||||
informer, err := db.settingsMgr.GetClusterInformer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cluster informer: %w", err)
|
||||
}
|
||||
if server == appv1.KubernetesInternalAPIServerAddr {
|
||||
inClusterEnabled, err := db.settingsMgr.IsInClusterEnabled()
|
||||
if err != nil {
|
||||
@@ -284,19 +288,27 @@ func (db *db) GetProjectClusters(_ context.Context, project string) ([]*appv1.Cl
|
||||
}
|
||||
|
||||
func (db *db) GetClusterServersByName(_ context.Context, name string) ([]string, error) {
|
||||
informer := db.settingsMgr.GetClusterInformer()
|
||||
informer, err := db.settingsMgr.GetClusterInformer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cluster informer: %w", err)
|
||||
}
|
||||
servers, err := informer.GetClusterServersByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// attempt to short circuit if the in-cluster name is not involved
|
||||
if name != appv1.KubernetesInClusterName && !slices.Contains(servers, appv1.KubernetesInternalAPIServerAddr) {
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
inClusterEnabled, err := db.settingsMgr.IsInClusterEnabled()
|
||||
if err != nil {
|
||||
log.Warnf(errCheckingInClusterEnabled, "GetClusterServersByName", err)
|
||||
return nil, fmt.Errorf(errCheckingInClusterEnabled, "GetClusterServersByName", err)
|
||||
}
|
||||
|
||||
// Handle local cluster special case
|
||||
if len(servers) == 0 && name == "in-cluster" && inClusterEnabled {
|
||||
if len(servers) == 0 && name == appv1.KubernetesInClusterName && inClusterEnabled {
|
||||
return []string{appv1.KubernetesInternalAPIServerAddr}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -661,6 +661,70 @@ func TestGetClusterServersByName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetClusterServersByName_IsInClusterEnabledLazyLoad(t *testing.T) {
|
||||
argoCDSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": nil,
|
||||
"server.secretkey": nil,
|
||||
},
|
||||
}
|
||||
prodSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-secret",
|
||||
Namespace: fakeNamespace,
|
||||
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeCluster},
|
||||
Annotations: map[string]string{
|
||||
common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"name": []byte("prod"),
|
||||
"server": []byte("https://prod.example.com"),
|
||||
"config": []byte("{}"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
clusterName string
|
||||
wantErr bool
|
||||
wantServers []string
|
||||
}{
|
||||
{
|
||||
name: "non in-cluster name does not call IsInClusterEnabled()",
|
||||
clusterName: "prod",
|
||||
wantErr: false,
|
||||
wantServers: []string{"https://prod.example.com"},
|
||||
},
|
||||
{
|
||||
name: "in-cluster name calls IsInClusterEnabled()",
|
||||
clusterName: "in-cluster",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
// argocd-cm is intentionally absent: IsInClusterEnabled() fails if called.
|
||||
kubeclientset := fake.NewClientset(argoCDSecret, prodSecret)
|
||||
db := NewDB(fakeNamespace, settings.NewSettingsManager(t.Context(), kubeclientset, fakeNamespace), kubeclientset)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
servers, err := db.GetClusterServersByName(t.Context(), tt.clusterName)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tt.wantServers, servers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCluster_MissingServerSecretKey(t *testing.T) {
|
||||
emptyArgoCDConfigMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
||||
@@ -1069,3 +1069,7 @@ func FormatAccessTokenCacheKey(sub string) string {
|
||||
func formatOidcTokenCacheKey(sub string, sid string) string {
|
||||
return fmt.Sprintf("%s_%s_%s", OidcTokenCachePrefix, sub, sid)
|
||||
}
|
||||
|
||||
func (a *ClientApp) IssuerURL() string {
|
||||
return a.issuerURL
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//go:build race
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -42,7 +41,7 @@ func TestClusterInformer_ConcurrentAccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secret1)
|
||||
clientset := fake.NewClientset(secret1)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -50,16 +49,15 @@ func TestClusterInformer_ConcurrentAccess(t *testing.T) {
|
||||
cache.WaitForCacheSync(ctx.Done(), informer.HasSynced)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for range 100 {
|
||||
wg.Go(func() {
|
||||
cluster, err := informer.GetClusterByURL("https://cluster1.example.com")
|
||||
assert.NoError(t, err)
|
||||
// require calls t.FailNow(), which only stops the current goroutine, not the test
|
||||
assert.NoError(t, err) //nolint:testifylint
|
||||
assert.NotNil(t, cluster)
|
||||
// Modifying returned cluster should not affect others due to DeepCopy
|
||||
cluster.Name = "modified"
|
||||
}()
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
@@ -87,7 +85,7 @@ func TestClusterInformer_TransformErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(badSecret)
|
||||
clientset := fake.NewClientset(badSecret)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -96,12 +94,12 @@ func TestClusterInformer_TransformErrors(t *testing.T) {
|
||||
|
||||
// GetClusterByURL should return not found since transform failed
|
||||
_, err = informer.GetClusterByURL("https://bad.example.com")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
|
||||
// ListClusters should return an error because the cache contains a secret and not a cluster
|
||||
_, err = informer.ListClusters()
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cluster cache contains unexpected type")
|
||||
}
|
||||
|
||||
@@ -140,7 +138,7 @@ func TestClusterInformer_TransformErrors_MixedSecrets(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(goodSecret, badSecret)
|
||||
clientset := fake.NewClientset(goodSecret, badSecret)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -154,7 +152,7 @@ func TestClusterInformer_TransformErrors_MixedSecrets(t *testing.T) {
|
||||
|
||||
// But ListClusters should fail because there's a bad secret in the cache
|
||||
_, err = informer.ListClusters()
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cluster cache contains unexpected type")
|
||||
}
|
||||
|
||||
@@ -177,7 +175,7 @@ func TestClusterInformer_DynamicUpdates(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secret1)
|
||||
clientset := fake.NewClientset(secret1)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -235,7 +233,7 @@ func TestClusterInformer_URLNormalization(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secret)
|
||||
clientset := fake.NewClientset(secret)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -290,7 +288,7 @@ func TestClusterInformer_GetClusterServersByName(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secrets...)
|
||||
clientset := fake.NewClientset(secrets...)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -309,7 +307,7 @@ func TestClusterInformer_RaceCondition(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
var secrets []*corev1.Secret
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("cluster-%d", i),
|
||||
@@ -319,15 +317,15 @@ func TestClusterInformer_RaceCondition(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"server": []byte(fmt.Sprintf("https://cluster%d.example.com", i)),
|
||||
"name": []byte(fmt.Sprintf("cluster-%d", i)),
|
||||
"server": fmt.Appendf(nil, "https://cluster%d.example.com", i),
|
||||
"name": fmt.Appendf(nil, "cluster-%d", i),
|
||||
"config": []byte(`{"bearerToken":"token"}`),
|
||||
},
|
||||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset()
|
||||
clientset := fake.NewClientset()
|
||||
for _, secret := range secrets {
|
||||
_, err := clientset.CoreV1().Secrets("argocd").Create(t.Context(), secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -342,11 +340,11 @@ func TestClusterInformer_RaceCondition(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
var readErrors, updateErrors atomic.Int64
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
for i := range 50 {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
for j := range 100 {
|
||||
clusterID := j % 10
|
||||
url := fmt.Sprintf("https://cluster%d.example.com", clusterID)
|
||||
|
||||
@@ -376,13 +374,13 @@ func TestClusterInformer_RaceCondition(t *testing.T) {
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 20; j++ {
|
||||
for j := range 20 {
|
||||
secret := secrets[id%10].DeepCopy()
|
||||
secret.Data["name"] = []byte(fmt.Sprintf("updated-%d-%d", id, j))
|
||||
secret.Data["name"] = fmt.Appendf(nil, "updated-%d-%d", id, j)
|
||||
|
||||
_, err := clientset.CoreV1().Secrets("argocd").Update(t.Context(), secret, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
@@ -393,11 +391,9 @@ func TestClusterInformer_RaceCondition(t *testing.T) {
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 50; j++ {
|
||||
for range 20 {
|
||||
wg.Go(func() {
|
||||
for range 50 {
|
||||
clusters, err := informer.ListClusters()
|
||||
if err != nil {
|
||||
readErrors.Add(1)
|
||||
@@ -412,7 +408,7 @@ func TestClusterInformer_RaceCondition(t *testing.T) {
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -444,7 +440,7 @@ func TestClusterInformer_DeepCopyIsolation(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secret)
|
||||
clientset := fake.NewClientset(secret)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -483,12 +479,13 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
name: "Empty namespace - no clusters",
|
||||
secrets: []runtime.Object{},
|
||||
testFunc: func(t *testing.T, informer *ClusterInformer) {
|
||||
t.Helper()
|
||||
clusters, err := informer.ListClusters()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, clusters)
|
||||
|
||||
_, err = informer.GetClusterByURL("https://nonexistent.example.com")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
},
|
||||
},
|
||||
@@ -511,9 +508,10 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testFunc: func(t *testing.T, informer *ClusterInformer) {
|
||||
t.Helper()
|
||||
cluster, err := informer.GetClusterByURL("https://noname.example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", cluster.Name)
|
||||
assert.Empty(t, cluster.Name)
|
||||
|
||||
servers, err := informer.GetClusterServersByName("")
|
||||
require.NoError(t, err)
|
||||
@@ -539,6 +537,7 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testFunc: func(t *testing.T, informer *ClusterInformer) {
|
||||
t.Helper()
|
||||
cluster, err := informer.GetClusterByURL("https://cluster.example.com:8443/path/")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "special", cluster.Name)
|
||||
@@ -578,6 +577,7 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testFunc: func(t *testing.T, informer *ClusterInformer) {
|
||||
t.Helper()
|
||||
cluster, err := informer.GetClusterByURL("https://duplicate.example.com")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cluster)
|
||||
@@ -598,20 +598,21 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
"server": []byte("https://many-ns.example.com"),
|
||||
"name": []byte("many-ns"),
|
||||
"namespaces": func() []byte {
|
||||
ns := ""
|
||||
for i := 0; i < 100; i++ {
|
||||
var sb strings.Builder
|
||||
for i := range 100 {
|
||||
if i > 0 {
|
||||
ns += ","
|
||||
sb.WriteString(",")
|
||||
}
|
||||
ns += fmt.Sprintf("namespace-%d", i)
|
||||
fmt.Fprintf(&sb, "namespace-%d", i)
|
||||
}
|
||||
return []byte(ns)
|
||||
return []byte(sb.String())
|
||||
}(),
|
||||
"config": []byte(`{}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
testFunc: func(t *testing.T, informer *ClusterInformer) {
|
||||
t.Helper()
|
||||
cluster, err := informer.GetClusterByURL("https://many-ns.example.com")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cluster.Namespaces, 100)
|
||||
@@ -648,6 +649,7 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
},
|
||||
},
|
||||
testFunc: func(t *testing.T, informer *ClusterInformer) {
|
||||
t.Helper()
|
||||
cluster, err := informer.GetClusterByURL("https://annotated.example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -676,7 +678,7 @@ func TestClusterInformer_EdgeCases(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
clientset := fake.NewSimpleClientset(tt.secrets...)
|
||||
clientset := fake.NewClientset(tt.secrets...)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -720,7 +722,7 @@ func TestClusterInformer_SecretDeletion(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secret1, secret2)
|
||||
clientset := fake.NewClientset(secret1, secret2)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -801,7 +803,7 @@ func TestClusterInformer_ComplexConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secret)
|
||||
clientset := fake.NewClientset(secret)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -814,8 +816,8 @@ func TestClusterInformer_ComplexConfig(t *testing.T) {
|
||||
assert.Equal(t, "admin", cluster.Config.Username)
|
||||
assert.Equal(t, "password123", cluster.Config.Password)
|
||||
assert.Equal(t, "bearer-token", cluster.Config.BearerToken)
|
||||
assert.True(t, cluster.Config.TLSClientConfig.Insecure)
|
||||
assert.Equal(t, "cluster.internal", cluster.Config.TLSClientConfig.ServerName)
|
||||
assert.True(t, cluster.Config.Insecure)
|
||||
assert.Equal(t, "cluster.internal", cluster.Config.ServerName)
|
||||
|
||||
assert.NotNil(t, cluster.Config.AWSAuthConfig)
|
||||
assert.Equal(t, "eks-cluster", cluster.Config.AWSAuthConfig.ClusterName)
|
||||
@@ -834,7 +836,7 @@ func BenchmarkClusterInformer_GetClusterByURL(b *testing.B) {
|
||||
defer cancel()
|
||||
|
||||
var secrets []runtime.Object
|
||||
for i := 0; i < 1000; i++ {
|
||||
for i := range 1000 {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("cluster-%d", i),
|
||||
@@ -844,15 +846,15 @@ func BenchmarkClusterInformer_GetClusterByURL(b *testing.B) {
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"server": []byte(fmt.Sprintf("https://cluster%d.example.com", i)),
|
||||
"name": []byte(fmt.Sprintf("cluster-%d", i)),
|
||||
"server": fmt.Appendf(nil, "https://cluster%d.example.com", i),
|
||||
"name": fmt.Appendf(nil, "cluster-%d", i),
|
||||
"config": []byte(`{"bearerToken":"token"}`),
|
||||
},
|
||||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
|
||||
clientset := fake.NewSimpleClientset(secrets...)
|
||||
clientset := fake.NewClientset(secrets...)
|
||||
informer, err := NewClusterInformer(clientset, "argocd")
|
||||
require.NoError(b, err)
|
||||
|
||||
|
||||
50
util/settings/impersonation.go
Normal file
50
util/settings/impersonation.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/glob"
|
||||
)
|
||||
|
||||
const (
|
||||
// serviceAccountDisallowedCharSet contains the characters that are not allowed to be present
|
||||
// in a DefaultServiceAccount configured for a DestinationServiceAccount
|
||||
serviceAccountDisallowedCharSet = "!*[]{}\\/"
|
||||
)
|
||||
|
||||
// DeriveServiceAccountToImpersonate determines the service account to be used for impersonation for the sync operation.
|
||||
// The returned service account will be fully qualified including namespace and the service account name in the format system:serviceaccount:<namespace>:<service_account>
|
||||
func DeriveServiceAccountToImpersonate(project *v1alpha1.AppProject, application *v1alpha1.Application, destCluster *v1alpha1.Cluster) (string, error) {
|
||||
// spec.Destination.Namespace is optional. If not specified, use the Application's
|
||||
// namespace
|
||||
serviceAccountNamespace := application.Spec.Destination.Namespace
|
||||
if serviceAccountNamespace == "" {
|
||||
serviceAccountNamespace = application.Namespace
|
||||
}
|
||||
// Loop through the destinationServiceAccounts and see if there is any destination that is a candidate.
|
||||
// if so, return the service account specified for that destination.
|
||||
for _, item := range project.Spec.DestinationServiceAccounts {
|
||||
dstServerMatched, err := glob.MatchWithError(item.Server, destCluster.Server)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid glob pattern for destination server: %w", err)
|
||||
}
|
||||
dstNamespaceMatched, err := glob.MatchWithError(item.Namespace, application.Spec.Destination.Namespace)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid glob pattern for destination namespace: %w", err)
|
||||
}
|
||||
if dstServerMatched && dstNamespaceMatched {
|
||||
if strings.Trim(item.DefaultServiceAccount, " ") == "" || strings.ContainsAny(item.DefaultServiceAccount, serviceAccountDisallowedCharSet) {
|
||||
return "", fmt.Errorf("default service account contains invalid chars '%s'", item.DefaultServiceAccount)
|
||||
} else if strings.Contains(item.DefaultServiceAccount, ":") {
|
||||
// service account is specified along with its namespace.
|
||||
return "system:serviceaccount:" + item.DefaultServiceAccount, nil
|
||||
}
|
||||
// service account needs to be prefixed with a namespace
|
||||
return fmt.Sprintf("system:serviceaccount:%s:%s", serviceAccountNamespace, item.DefaultServiceAccount), nil
|
||||
}
|
||||
}
|
||||
// if there is no match found in the AppProject.Spec.DestinationServiceAccounts, use the default service account of the destination namespace.
|
||||
return "", fmt.Errorf("no matching service account found for destination server %s and namespace %s", application.Spec.Destination.Server, serviceAccountNamespace)
|
||||
}
|
||||
268
util/settings/impersonation_test.go
Normal file
268
util/settings/impersonation_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestDeriveServiceAccountToImpersonate(t *testing.T) {
|
||||
t.Run("MatchingServerAndNamespace", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "https://cluster-api.example.com", Namespace: "dest-ns", DefaultServiceAccount: "test-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "system:serviceaccount:dest-ns:test-sa", user)
|
||||
})
|
||||
|
||||
t.Run("MatchingWithGlobPatterns", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "*", Namespace: "*", DefaultServiceAccount: "test-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "any-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "system:serviceaccount:any-ns:test-sa", user)
|
||||
})
|
||||
|
||||
t.Run("MatchingWithNamespacedServiceAccount", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "https://cluster-api.example.com", Namespace: "dest-ns", DefaultServiceAccount: "other-ns:deploy-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "system:serviceaccount:other-ns:deploy-sa", user)
|
||||
})
|
||||
|
||||
t.Run("FallbackToAppNamespaceWhenDestEmpty", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
// Namespace pattern matches empty string via glob "*"
|
||||
{Server: "*", Namespace: "", DefaultServiceAccount: "test-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "app-ns"},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
require.NoError(t, err)
|
||||
// Should use app.Namespace ("app-ns") as the SA namespace since Destination.Namespace is empty
|
||||
assert.Equal(t, "system:serviceaccount:app-ns:test-sa", user)
|
||||
})
|
||||
|
||||
t.Run("NoMatchingEntry", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "https://other-server.com", Namespace: "other-ns", DefaultServiceAccount: "test-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
assert.Empty(t, user)
|
||||
assert.ErrorContains(t, err, "no matching service account found")
|
||||
})
|
||||
|
||||
t.Run("EmptyDestinationServiceAccounts", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
assert.Empty(t, user)
|
||||
assert.ErrorContains(t, err, "no matching service account found")
|
||||
})
|
||||
|
||||
t.Run("InvalidServiceAccountChars", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "*", Namespace: "*", DefaultServiceAccount: "bad*sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
assert.Empty(t, user)
|
||||
assert.ErrorContains(t, err, "default service account contains invalid chars")
|
||||
})
|
||||
|
||||
t.Run("BlankServiceAccount", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "*", Namespace: "*", DefaultServiceAccount: " "},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
assert.Empty(t, user)
|
||||
assert.ErrorContains(t, err, "default service account contains invalid chars")
|
||||
})
|
||||
|
||||
t.Run("InvalidServerGlobPattern", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "[", Namespace: "dest-ns", DefaultServiceAccount: "test-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
assert.Empty(t, user)
|
||||
assert.ErrorContains(t, err, "invalid glob pattern for destination server")
|
||||
})
|
||||
|
||||
t.Run("InvalidNamespaceGlobPattern", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "*", Namespace: "[", DefaultServiceAccount: "test-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
assert.Empty(t, user)
|
||||
assert.ErrorContains(t, err, "invalid glob pattern for destination namespace")
|
||||
})
|
||||
|
||||
t.Run("FirstMatchWins", func(t *testing.T) {
|
||||
project := &v1alpha1.AppProject{
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
DestinationServiceAccounts: []v1alpha1.ApplicationDestinationServiceAccount{
|
||||
{Server: "*", Namespace: "dest-ns", DefaultServiceAccount: "first-sa"},
|
||||
{Server: "*", Namespace: "*", DefaultServiceAccount: "second-sa"},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://cluster-api.example.com",
|
||||
Namespace: "dest-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
cluster := &v1alpha1.Cluster{Server: "https://cluster-api.example.com"}
|
||||
|
||||
user, err := DeriveServiceAccountToImpersonate(project, app, cluster)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "system:serviceaccount:dest-ns:first-sa", user)
|
||||
})
|
||||
}
|
||||
@@ -663,10 +663,11 @@ func (mgr *SettingsManager) GetSecretsInformer() (cache.SharedIndexInformer, err
|
||||
}
|
||||
|
||||
// GetClusterInformer returns the cluster cache for optimized cluster lookups.
|
||||
func (mgr *SettingsManager) GetClusterInformer() *ClusterInformer {
|
||||
// Ensure the settings manager is initialized
|
||||
_ = mgr.ensureSynced(false)
|
||||
return mgr.clusterInformer
|
||||
func (mgr *SettingsManager) GetClusterInformer() (*ClusterInformer, error) {
|
||||
if err := mgr.ensureSynced(false); err != nil {
|
||||
return nil, fmt.Errorf("error ensuring that the settings manager is synced: %w", err)
|
||||
}
|
||||
return mgr.clusterInformer, nil
|
||||
}
|
||||
|
||||
func (mgr *SettingsManager) updateSecret(callback func(*corev1.Secret) error) error {
|
||||
|
||||
Reference in New Issue
Block a user