Compare commits

...

18 Commits

Author SHA1 Message Date
github-actions[bot]
ec60abd4d8 Bump version to 2.13.0-rc3 (#20268)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-10-07 09:32:14 -04:00
gcp-cherry-pick-bot[bot]
c6d9d50ee9 fix: Rework git tag semver resolution (#20083) (#20096) (#20213)
* Write initial tests



* Improve git tag semver resolution



* Add company to list of users



* Fix broken error string check



* Fix incorrect semver test assumption



* switch to debug statement



* Add more testcases for review



* review comments



---------

Signed-off-by: Paul Larsen <pnvlarsen@gmail.com>
Co-authored-by: Paul Larsen <pnvlarsen@gmail.com>
2024-10-06 23:49:01 -04:00
gcp-cherry-pick-bot[bot]
7244b8b40f fix: Policy/policy.open-cluster-management.io health check is broken (#20108) (#20109) (#20258)
Tried using the health check as listed here but it gave error:

| error setting app health: failed to get resource health for "Policy" with name "XXXX" in namespace "local-cluster": <string>:35: invalid value (nil) at index 1 in table for concat stack traceback: [G]: in function 'concat' <string>:35: in main chunk [G]: ?

This change fixes the error by updating how the noncompliant clusters are tracked and counted to use latest Lua recommendations.

Signed-off-by: Ian Tewksbury <itewk@redhat.com>
Co-authored-by: Ian Tewksbury <itewk@redhat.com>
2024-10-06 15:35:08 -04:00
gcp-cherry-pick-bot[bot]
8e81bb6c80 Fixes minor typo which lead to using the bearer token as api URL and was obviously not working. (#20169) (#20170)
Signed-off-by: Dan Garfield <dan@codefresh.io>
2024-10-06 13:00:11 +03:00
gcp-cherry-pick-bot[bot]
3bc2e1ae4c fix(health): only consider non-empty health checks (#20232) (#20235)
* fix(health): only consider non-empty health checks

For wildcard health checks, only consider wildcards with a non-empty
health check. Fixes #16905 (at least partially).



* test: renaming test case for clarity



* refactor: add clarity as to what the function is supposed to do



* Update docs/operator-manual/health.md




* test: add test case for `*/*` override with empty healthcheck



---------

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-10-04 15:35:47 -04:00
gcp-cherry-pick-bot[bot]
61f63f35ae chore: Added unit tests and fix e2e tests for application sync decoupling feature (#19966) (#20219)
* fixed doc comments and added unit tests



* Added comments for the newly added unit tests



* Refactored method name to deriveServiceAccountToImpersonate



* Using const name in return value



* Added unit tests for argocd proj add-destination-service-accounts



* Fixed failing e2e tests



* Fix linting errors



* Using require package instead of assert and fixed code generation



* Removed parallel execution of tests for sync with impersonate



* Added err checks for glob validations



* Fixed e2e tests for sync impersonation



* Using consistently based expects in E2E tests



* Added more unit tests and fixed go generate



* Fixed failed lint errors, unit and e2e test failures



* Fixed goimports linter issue



* Added code comments and added few missing unit tests



* Added missing unit test for GetDestinationServiceAccounts method



* Fixed goimports formatting with local for project_test.go



* Corrected typo in a field name additionalObjs



* Fixed failing unit tests



---------

Signed-off-by: anandf <anjoseph@redhat.com>
Co-authored-by: Anand Francis Joseph <anjoseph@redhat.com>
2024-10-03 15:38:17 -04:00
gcp-cherry-pick-bot[bot]
5eb1f9bd16 fix: update health check to support modelmesh (#20142) (#20218)
Signed-off-by: Trevor Royer <troyer@redhat.com>
Co-authored-by: Trevor Royer <troyer@redhat.com>
Co-authored-by: Dan Garfield <dan@codefresh.io>
2024-10-03 14:08:11 -04:00
gcp-cherry-pick-bot[bot]
4149f484bf fix: Fix false positive in plugin application discovery (#20196) (#20214)
* fix: fix false positive in plugin application discovery



* fix: apply suggestion to return immediately if discovery is not configured for unnamed plugin



---------

Signed-off-by: Pradithya Aria <pradithya.pura@gojek.com>
Co-authored-by: aria <pradithya.pura@gojek.com>
2024-10-03 13:25:32 -04:00
gcp-cherry-pick-bot[bot]
0b2895977e docs(ui): sorting version (#20181) (#20203)
Signed-off-by: nueavv <nuguni@kakao.com>
Co-authored-by: 1102 <90682513+nueavv@users.noreply.github.com>
2024-10-02 13:41:17 -04:00
gcp-cherry-pick-bot[bot]
99b30a87a6 fix: Fix argocd appset generate failure due to missing clusterrole (#20162) (#20164)
* fix: FIx argocd-server clusterrole to allow argocd appset generate using cluster generator



* fix: update generated code



---------

Signed-off-by: Pradithya Aria <pradithya.pura@gojek.com>
Co-authored-by: aria <pradithya.pura@gojek.com>
2024-09-30 09:42:43 -04:00
gcp-cherry-pick-bot[bot]
9fc6ec116d fix(extension): add header to support apps-in-any-namespace (#20123) (#20126)
Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2024-09-27 11:54:38 -04:00
gcp-cherry-pick-bot[bot]
f7f553f675 chore(deps): bump Helm from 3.15.2 to 3.15.4 (#20135) (#20137)
* sec: upgrade helm version in order to fix critical vulnerability



* sec: upgrade helm version in order to fix critical vulnerability



---------

Signed-off-by: pashakostohrys <pavel@codefresh.io>
Co-authored-by: pasha-codefresh <pavel@codefresh.io>
2024-09-27 09:48:59 -04:00
gcp-cherry-pick-bot[bot]
a9d9d07edd fix: CVE-2024-45296 Backtracking regular expressions cause ReDoS by upgrading path-to-regexp from 1.8.0 to 1.9.0 (#20087) (#20089)
Signed-off-by: Cheng Fang <cfang@redhat.com>
Co-authored-by: Cheng Fang <cfang@redhat.com>
2024-09-24 23:28:18 -04:00
github-actions[bot]
0f083c9e58 Bump version to 2.13.0-rc2 (#20029)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: pasha-codefresh <39732895+pasha-codefresh@users.noreply.github.com>
2024-09-20 14:36:29 +03:00
gcp-cherry-pick-bot[bot]
5392ca7e79 chore(deps): bump dompurify from 2.3.6 to 2.5.6 in /ui (#19955) (#20015)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.6 to 2.5.6.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.6...2.5.6)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 21:18:56 -04:00
gcp-cherry-pick-bot[bot]
243ecc2f25 fix: notification controller crash loop in 2.13 RC1 (#19984) (#19986)
Signed-off-by: pashakostohrys <pavel@codefresh.io>
Co-authored-by: pasha-codefresh <pavel@codefresh.io>
2024-09-18 23:01:51 +03:00
gcp-cherry-pick-bot[bot]
425b4087f3 fix: Add redis password to forwardCacheClient struct (#19599) (#19977)
Signed-off-by: Netanel Kadosh <kadoshnetanel@gmail.com>
Co-authored-by: Netanel Kadosh <kadoshnetanel@gmail.com>
2024-09-18 12:53:35 +03:00
github-actions[bot]
74a367d10e Bump version to 2.13.0-rc1 (#19943)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: pasha-codefresh <39732895+pasha-codefresh@users.noreply.github.com>
2024-09-16 09:54:17 +03:00
71 changed files with 2163 additions and 244 deletions

View File

@@ -41,6 +41,7 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Beez Innovation Labs](https://www.beezlabs.com/)
1. [Bedag Informatik AG](https://www.bedag.ch/)
1. [Beleza Na Web](https://www.belezanaweb.com.br/)
1. [Believable Bots](https://believablebots.io)
1. [BigPanda](https://bigpanda.io)
1. [BioBox Analytics](https://biobox.io)
1. [BMW Group](https://www.bmwgroup.com/)

View File

@@ -1 +1 @@
2.13.0
2.13.0-rc3

View File

@@ -168,7 +168,7 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
if err != nil {
return nil, fmt.Errorf("error fetching Secret Bearer token: %w", err)
}
return pullrequest.NewBitbucketServiceBearerToken(ctx, providerConfig.API, appToken, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
return pullrequest.NewBitbucketServiceBearerToken(ctx, appToken, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
} else if providerConfig.BasicAuth != nil {
password, err := utils.GetSecretRef(ctx, g.client, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
if err != nil {

2
assets/swagger.json generated
View File

@@ -6084,7 +6084,7 @@
"properties": {
"defaultServiceAccount": {
"type": "string",
"title": "ServiceAccountName to be used for impersonation during the sync operation"
"title": "DefaultServiceAccount to be used for impersonation during the sync operation"
},
"namespace": {
"description": "Namespace specifies the target namespace for the application's resources.",

View File

@@ -163,6 +163,7 @@ func NewCommand() *cobra.Command {
}()
go ctrl.Run(ctx, processorsCount)
<-ctx.Done()
return nil
},
}

View File

@@ -48,6 +48,7 @@ type forwardCacheClient struct {
err error
redisHaProxyName string
redisName string
redisPassword string
}
func (c *forwardCacheClient) doLazy(action func(client cache.CacheClient) error) error {
@@ -64,7 +65,7 @@ func (c *forwardCacheClient) doLazy(action func(client cache.CacheClient) error)
return
}
redisClient := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("localhost:%d", redisPort)})
redisClient := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("localhost:%d", redisPort), Password: c.redisPassword})
c.client = cache.NewRedisCache(redisClient, time.Hour, c.compression)
})
if c.err != nil {
@@ -251,12 +252,12 @@ func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOpti
if err != nil {
return fmt.Errorf("error running miniredis: %w", err)
}
appstateCache := appstatecache.NewCache(cache.NewCache(&forwardCacheClient{namespace: namespace, context: ctxStr, compression: compression, redisHaProxyName: clientOpts.RedisHaProxyName, redisName: clientOpts.RedisName}), time.Hour)
redisOptions := &redis.Options{Addr: mr.Addr()}
if err = common.SetOptionalRedisPasswordFromKubeConfig(ctx, kubeClientset, namespace, redisOptions); err != nil {
log.Warnf("Failed to fetch & set redis password for namespace %s: %v", namespace, err)
}
appstateCache := appstatecache.NewCache(cache.NewCache(&forwardCacheClient{namespace: namespace, context: ctxStr, compression: compression, redisHaProxyName: clientOpts.RedisHaProxyName, redisName: clientOpts.RedisName, redisPassword: redisOptions.Password}), time.Hour)
srv := server.NewServer(ctx, server.ArgoCDServerOpts{
EnableGZip: false,
Namespace: namespace,

View File

@@ -48,6 +48,8 @@ func AddProjFlags(command *cobra.Command, opts *ProjectOpts) {
command.Flags().StringArrayVar(&opts.allowedNamespacedResources, "allow-namespaced-resource", []string{}, "List of allowed namespaced resources")
command.Flags().StringArrayVar(&opts.deniedNamespacedResources, "deny-namespaced-resource", []string{}, "List of denied namespaced resources")
command.Flags().StringSliceVar(&opts.SourceNamespaces, "source-namespaces", []string{}, "List of source namespaces for applications")
command.Flags().StringArrayVar(&opts.destinationServiceAccounts, "dest-service-accounts", []string{},
"Destination server, namespace and target service account (e.g. https://192.168.99.100:8443,default,default-sa)")
}
func getGroupKindList(values []string) []v1.GroupKind {
@@ -98,8 +100,8 @@ func (opts *ProjectOpts) GetDestinationServiceAccounts() []v1alpha1.ApplicationD
destinationServiceAccounts := make([]v1alpha1.ApplicationDestinationServiceAccount, 0)
for _, destStr := range opts.destinationServiceAccounts {
parts := strings.Split(destStr, ",")
if len(parts) != 2 {
log.Fatalf("Expected destination of the form: server,namespace. Received: %s", destStr)
if len(parts) != 3 {
log.Fatalf("Expected destination service account of the form: server,namespace, defaultServiceAccount. Received: %s", destStr)
} else {
destinationServiceAccounts = append(destinationServiceAccounts, v1alpha1.ApplicationDestinationServiceAccount{
Server: parts[0],

View File

@@ -5,6 +5,8 @@ import (
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestProjectOpts_ResourceLists(t *testing.T) {
@@ -22,3 +24,27 @@ func TestProjectOpts_ResourceLists(t *testing.T) {
[]v1.GroupKind{{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"}}, opts.GetDeniedClusterResources(),
)
}
func TestProjectOpts_GetDestinationServiceAccounts(t *testing.T) {
opts := ProjectOpts{
destinationServiceAccounts: []string{
"https://192.168.99.100:8443,test-ns,test-sa",
"https://kubernetes.default.svc.local:6443,guestbook,guestbook-sa",
},
}
assert.ElementsMatch(t,
[]v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://192.168.99.100:8443",
Namespace: "test-ns",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.default.svc.local:6443",
Namespace: "guestbook",
DefaultServiceAccount: "guestbook-sa",
},
}, opts.GetDestinationServiceAccounts(),
)
}

View File

@@ -64,6 +64,7 @@ type fakeData struct {
metricsCacheExpiration time.Duration
applicationNamespaces []string
updateRevisionForPathsResponse *apiclient.UpdateRevisionForPathsResponse
additionalObjs []runtime.Object
}
type MockKubectl struct {
@@ -133,7 +134,9 @@ func newFakeController(data *fakeData, repoErr error) *ApplicationController {
},
Data: data.configMapData,
}
kubeClient := fake.NewSimpleClientset(&clust, &cm, &secret)
runtimeObjs := []runtime.Object{&clust, &secret, &cm}
runtimeObjs = append(runtimeObjs, data.additionalObjs...)
kubeClient := fake.NewSimpleClientset(runtimeObjs...)
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
kubectl := &MockKubectl{Kubectl: &kubetest.MockKubectlCmd{}}
ctrl, err := NewApplicationController(

View File

@@ -44,6 +44,10 @@ 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 string) (openapi.Resources, error) {
@@ -287,8 +291,13 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
}
trackingMethod := argo.GetTrackingMethod(m.settingsMgr)
if m.settingsMgr.IsImpersonationEnabled() {
serviceAccountToImpersonate, err := deriveServiceAccountName(proj, app)
impersonationEnabled, err := m.settingsMgr.IsImpersonationEnabled()
if err != nil {
log.Errorf("could not get impersonation feature flag: %v", err)
return
}
if impersonationEnabled {
serviceAccountToImpersonate, err := deriveServiceAccountToImpersonate(proj, app)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("failed to find a matching service account to impersonate: %v", err)
@@ -557,9 +566,9 @@ func syncWindowPreventsSync(app *v1alpha1.Application, proj *v1alpha1.AppProject
return !window.CanSync(isManual)
}
// deriveServiceAccountName determines the service account to be used for impersonation for the sync operation.
// 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 deriveServiceAccountName(project *v1alpha1.AppProject, application *v1alpha1.Application) (string, error) {
func deriveServiceAccountToImpersonate(project *v1alpha1.AppProject, application *v1alpha1.Application) (string, error) {
// spec.Destination.Namespace is optional. If not specified, use the Application's
// namespace
serviceAccountNamespace := application.Spec.Destination.Namespace
@@ -569,10 +578,18 @@ func deriveServiceAccountName(project *v1alpha1.AppProject, application *v1alpha
// 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 := glob.Match(item.Server, application.Spec.Destination.Server)
dstNamespaceMatched := glob.Match(item.Namespace, application.Spec.Destination.Namespace)
dstServerMatched, err := glob.MatchWithError(item.Server, application.Spec.Destination.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.Contains(item.DefaultServiceAccount, ":") {
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 fmt.Sprintf("system:serviceaccount:%s", item.DefaultServiceAccount), nil
} else {

View File

@@ -2,6 +2,7 @@ package controller
import (
"context"
"strconv"
"testing"
"github.com/argoproj/gitops-engine/pkg/sync"
@@ -9,6 +10,7 @@ import (
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -644,6 +646,771 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
})
}
func TestDeriveServiceAccountMatchingNamespaces(t *testing.T) {
type fixture struct {
project *v1alpha1.AppProject
application *v1alpha1.Application
}
setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
project := &v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd-ns",
Name: "testProj",
},
Spec: v1alpha1.AppProjectSpec{
DestinationServiceAccounts: destinationServiceAccounts,
},
}
app := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Namespace: applicationNamespace,
Name: "testApp",
},
Spec: v1alpha1.ApplicationSpec{
Project: "testProj",
Destination: v1alpha1.ApplicationDestination{
Server: destinationServerURL,
Namespace: destinationNamespace,
},
},
}
return &fixture{
project: project,
application: app,
}
}
t.Run("empty destination service accounts", func(t *testing.T) {
// given an application referring a project with no destination service accounts
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := ""
expectedErrMsg := "no matching service account found for destination server https://kubernetes.svc.local and namespace testns"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
assert.Equal(t, expectedSA, sa)
// then, there should be an error saying no valid match was found
assert.EqualError(t, err, expectedErrMsg)
})
t.Run("exact match of destination namespace", func(t *testing.T) {
// given an application referring a project with exactly one destination service account that matches the application destination,
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should be no error and should use the right service account for impersonation
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts having one exact match for application destination
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "guestbook",
DefaultServiceAccount: "guestbook-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "guestbook-test",
DefaultServiceAccount: "guestbook-test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should be no error and should use the right service account for impersonation
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts having multiple match for application destination
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa-3",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "guestbook",
DefaultServiceAccount: "guestbook-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should be no error and it should use the first matching service account for impersonation
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts with glob patterns matching the application destination
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "test*",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and should use the first matching glob pattern service account for impersonation
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("no match among a valid list", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts with no matches for application destination
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "test1",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "test2",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := ""
expectedErrMsg := "no matching service account found for destination server https://kubernetes.svc.local and namespace testns"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should be an error saying no match was found
require.EqualError(t, err, expectedErrMsg)
assert.Equal(t, expectedSA, sa)
})
t.Run("app destination namespace is empty", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts with empty application destination namespace
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "*",
DefaultServiceAccount: "test-sa-2",
},
}
destinationNamespace := ""
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:argocd-ns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and the service account configured for with empty namespace should be used.
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("match done via catch all glob pattern", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts having a catch all glob pattern
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns1",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "*",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and the catch all service account should be returned
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("match done via invalid glob pattern", func(t *testing.T) {
// given an application referring a project with a destination service account having an invalid glob pattern for namespace
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "e[[a*",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := ""
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there must be an error as the glob pattern is invalid.
require.ErrorContains(t, err, "invalid glob pattern for destination namespace")
assert.Equal(t, expectedSA, sa)
})
t.Run("sa specified with a namespace", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts having a matching service account specified with its namespace
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "myns:test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "*",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:myns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
assert.Equal(t, expectedSA, sa)
// then, there should not be any error and the service account with its namespace should be returned.
require.NoError(t, err)
})
}
func TestDeriveServiceAccountMatchingServers(t *testing.T) {
type fixture struct {
project *v1alpha1.AppProject
application *v1alpha1.Application
}
setup := func(destinationServiceAccounts []v1alpha1.ApplicationDestinationServiceAccount, destinationNamespace, destinationServerURL, applicationNamespace string) *fixture {
project := &v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd-ns",
Name: "testProj",
},
Spec: v1alpha1.AppProjectSpec{
DestinationServiceAccounts: destinationServiceAccounts,
},
}
app := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Namespace: applicationNamespace,
Name: "testApp",
},
Spec: v1alpha1.ApplicationSpec{
Project: "testProj",
Destination: v1alpha1.ApplicationDestination{
Server: destinationServerURL,
Namespace: destinationNamespace,
},
},
}
return &fixture{
project: project,
application: app,
}
}
t.Run("exact one match with multiple destination service accounts", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts and one exact match for application destination
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "guestbook",
DefaultServiceAccount: "guestbook-sa",
},
{
Server: "https://abc.svc.local",
Namespace: "guestbook",
DefaultServiceAccount: "guestbook-test-sa",
},
{
Server: "https://cde.svc.local",
Namespace: "guestbook",
DefaultServiceAccount: "default-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and the right service account must be returned.
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("first match to be used when multiple matches are available", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts and multiple matches for application destination
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "guestbook",
DefaultServiceAccount: "guestbook-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and first matching service account should be used
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("first match to be used when glob pattern is used", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts with a matching glob pattern and exact match
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "test*",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
assert.Equal(t, expectedSA, sa)
// then, there should not be any error and the service account of the glob pattern, being the first match should be returned.
require.NoError(t, err)
})
t.Run("no match among a valid list", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts with no match
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa",
},
{
Server: "https://abc.svc.local",
Namespace: "testns",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://cde.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://xyz.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := ""
expectedErr := "no matching service account found for destination server https://xyz.svc.local and namespace testns"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there an error with appropriate message must be returned
require.EqualError(t, err, expectedErr)
assert.Equal(t, expectedSA, sa)
})
t.Run("match done via catch all glob pattern", func(t *testing.T) {
// given an application referring a project with multiple destination service accounts with matching catch all glob pattern
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://kubernetes.svc.local",
Namespace: "testns1",
DefaultServiceAccount: "test-sa-2",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
{
Server: "*",
Namespace: "*",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://localhost:6443"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:testns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and the service account of the glob pattern match must be returned.
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
t.Run("match done via invalid glob pattern", func(t *testing.T) {
// given an application referring a project with a destination service account having an invalid glob pattern for server
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "e[[a*",
Namespace: "test-ns",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://kubernetes.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := ""
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there must be an error as the glob pattern is invalid.
require.ErrorContains(t, err, "invalid glob pattern for destination server")
assert.Equal(t, expectedSA, sa)
})
t.Run("sa specified with a namespace", func(t *testing.T) {
// given app sync impersonation feature is enabled and matching service account is prefixed with a namespace
t.Parallel()
destinationServiceAccounts := []v1alpha1.ApplicationDestinationServiceAccount{
{
Server: "https://abc.svc.local",
Namespace: "testns",
DefaultServiceAccount: "myns:test-sa",
},
{
Server: "https://kubernetes.svc.local",
Namespace: "default",
DefaultServiceAccount: "default-sa",
},
{
Server: "*",
Namespace: "*",
DefaultServiceAccount: "test-sa",
},
}
destinationNamespace := "testns"
destinationServerURL := "https://abc.svc.local"
applicationNamespace := "argocd-ns"
expectedSA := "system:serviceaccount:myns:test-sa"
f := setup(destinationServiceAccounts, destinationNamespace, destinationServerURL, applicationNamespace)
// when
sa, err := deriveServiceAccountToImpersonate(f.project, f.application)
// then, there should not be any error and the service account with the given namespace prefix must be returned.
require.NoError(t, err)
assert.Equal(t, expectedSA, sa)
})
}
func TestSyncWithImpersonate(t *testing.T) {
type fixture struct {
project *v1alpha1.AppProject
application *v1alpha1.Application
controller *ApplicationController
}
setup := func(impersonationEnabled bool, destinationNamespace, serviceAccountName string) *fixture {
app := newFakeApp()
app.Status.OperationState = nil
app.Status.History = nil
project := &v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{
Namespace: test.FakeArgoCDNamespace,
Name: "default",
},
Spec: v1alpha1.AppProjectSpec{
DestinationServiceAccounts: []v1alpha1.
ApplicationDestinationServiceAccount{
{
Server: "https://localhost:6443",
Namespace: destinationNamespace,
DefaultServiceAccount: serviceAccountName,
},
},
},
}
additionalObjs := []runtime.Object{}
if serviceAccountName != "" {
syncServiceAccount := &corev1.ServiceAccount{
ObjectMeta: v1.ObjectMeta{
Name: serviceAccountName,
Namespace: test.FakeDestNamespace,
},
}
additionalObjs = append(additionalObjs, syncServiceAccount)
}
data := fakeData{
apps: []runtime.Object{app, project},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: "https://localhost:6443",
Revision: "abc123",
},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
configMapData: map[string]string{
"application.sync.impersonation.enabled": strconv.FormatBool(impersonationEnabled),
},
additionalObjs: additionalObjs,
}
ctrl := newFakeController(&data, nil)
return &fixture{
project: project,
application: app,
controller: ctrl,
}
}
t.Run("sync with impersonation and no matching service account", func(t *testing.T) {
// given app sync impersonation feature is enabled with an application referring a project no matching service account
f := setup(true, test.FakeArgoCDNamespace, "")
opMessage := "failed to find a matching service account to impersonate: no matching service account found for destination server https://localhost:6443 and namespace fake-dest-ns"
opState := &v1alpha1.OperationState{
Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
},
Phase: common.OperationRunning,
}
// when
f.controller.appStateManager.SyncAppState(f.application, opState)
// then, app sync should fail with expected error message in operation state
assert.Equal(t, common.OperationError, opState.Phase)
assert.Contains(t, opState.Message, opMessage)
})
t.Run("sync with impersonation and empty service account match", func(t *testing.T) {
// given app sync impersonation feature is enabled with an application referring a project matching service account that is an empty string
f := setup(true, test.FakeDestNamespace, "")
opMessage := "failed to find a matching service account to impersonate: default service account contains invalid chars ''"
opState := &v1alpha1.OperationState{
Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
},
Phase: common.OperationRunning,
}
// when
f.controller.appStateManager.SyncAppState(f.application, opState)
// then app sync should fail with expected error message in operation state
assert.Equal(t, common.OperationError, opState.Phase)
assert.Contains(t, opState.Message, opMessage)
})
t.Run("sync with impersonation and matching sa", func(t *testing.T) {
// given app sync impersonation feature is enabled with an application referring a project matching service account
f := setup(true, test.FakeDestNamespace, "test-sa")
opMessage := "successfully synced (no more tasks)"
opState := &v1alpha1.OperationState{
Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
},
Phase: common.OperationRunning,
}
// when
f.controller.appStateManager.SyncAppState(f.application, opState)
// then app sync should not fail
assert.Equal(t, common.OperationSucceeded, opState.Phase)
assert.Contains(t, opState.Message, opMessage)
})
t.Run("sync without impersonation", func(t *testing.T) {
// given app sync impersonation feature is disabled with an application referring a project matching service account
f := setup(false, test.FakeDestNamespace, "")
opMessage := "successfully synced (no more tasks)"
opState := &v1alpha1.OperationState{
Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
},
Phase: common.OperationRunning,
}
// when
f.controller.appStateManager.SyncAppState(f.application, opState)
// then application sync should pass using the control plane service account
assert.Equal(t, common.OperationSucceeded, opState.Phase)
assert.Contains(t, opState.Message, opMessage)
})
}
func dig[T any](obj interface{}, path []interface{}) T {
i := obj

View File

@@ -32,23 +32,41 @@ function initializeVersionDropdown() {
window[callbackName] = function(response) {
const div = document.createElement('div');
div.innerHTML = response.html;
document.querySelector(".md-header__inner > .md-header__title").appendChild(div);
const headerTitle = document.querySelector(".md-header__inner > .md-header__title");
if (headerTitle) {
headerTitle.appendChild(div);
}
const container = div.querySelector('.rst-versions');
if (!container) return; // Exit if container not found
// Add caret icon
var caret = document.createElement('div');
caret.innerHTML = "<i class='fa fa-caret-down dropdown-caret'></i>";
caret.classList.add('dropdown-caret');
div.querySelector('.rst-current-version').appendChild(caret);
const currentVersionElem = div.querySelector('.rst-current-version');
if (currentVersionElem) {
currentVersionElem.appendChild(caret);
}
div.querySelector('.rst-current-version').addEventListener('click', function() {
container.classList.toggle('shift-up');
});
// Add click listener to toggle dropdown
if (currentVersionElem && container) {
currentVersionElem.addEventListener('click', function() {
container.classList.toggle('shift-up');
});
}
// Sorting Logic
sortVersionLinks(container);
};
// Load CSS
var CSSLink = document.createElement('link');
CSSLink.rel = 'stylesheet';
CSSLink.href = '/assets/versions.css';
document.getElementsByTagName('head')[0].appendChild(CSSLink);
// Load JSONP Script
var script = document.createElement('script');
const currentVersion = getCurrentVersion();
script.src = 'https://argo-cd.readthedocs.io/_/api/v2/footer_html/?' +
@@ -56,6 +74,58 @@ function initializeVersionDropdown() {
document.getElementsByTagName('head')[0].appendChild(script);
}
// Function to sort version links
function sortVersionLinks(container) {
// Find all <dl> elements within the container
const dlElements = container.querySelectorAll('dl');
dlElements.forEach(dl => {
const dt = dl.querySelector('dt');
if (dt && dt.textContent.trim().toLowerCase() === 'versions') {
// Found the Versions <dl>
const ddElements = Array.from(dl.querySelectorAll('dd'));
// Define sorting criteria
ddElements.sort((a, b) => {
const aText = a.textContent.trim().toLowerCase();
const bText = b.textContent.trim().toLowerCase();
// Prioritize 'latest' and 'stable'
if (aText === 'latest') return -1;
if (bText === 'latest') return 1;
if (aText === 'stable') return -1;
if (bText === 'stable') return 1;
// Extract version numbers (e.g., release-2.9)
const aVersionMatch = aText.match(/release-(\d+(\.\d+)*)/);
const bVersionMatch = bText.match(/release-(\d+(\.\d+)*)/);
if (aVersionMatch && bVersionMatch) {
const aVersion = aVersionMatch[1].split('.').map(Number);
const bVersion = bVersionMatch[1].split('.').map(Number);
for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) {
const aNum = aVersion[i] || 0;
const bNum = bVersion[i] || 0;
if (aNum > bNum) return -1;
if (aNum < bNum) return 1;
}
return 0;
}
// Fallback to alphabetical order
return aText.localeCompare(bText);
});
// Remove existing <dd> elements
ddElements.forEach(dd => dl.removeChild(dd));
// Append sorted <dd> elements
ddElements.forEach(dd => dl.appendChild(dd));
}
});
}
// VERSION WARNINGS
window.addEventListener("DOMContentLoaded", function() {
var margin = 30;

View File

@@ -1,7 +1,7 @@
# Application Sync using impersonation
!!! warning "Alpha Feature"
This is an experimental, alpha-quality feature that allows you to control the service account used for the sync operation. The configured service account, could have lesser privileges required for creating resources compared to the highly privileged access required for the control plane operations.
This is an experimental, alpha-quality feature that allows you to control the service account used for the sync operation. The configured service account could have lesser privileges required for creating resources compared to the highly privileged access required for the control plane operations.
!!! warning
Please read this documentation carefully before you enable this feature. Misconfiguration could lead to potential security issues.
@@ -94,7 +94,7 @@ spec:
sourceRepos:
- '*'
destinations:
- *
- '*'
destinationServiceAccounts:
- server: https://kubernetes.default.svc
namespace: guestbook

View File

@@ -329,14 +329,14 @@ data:
# spread out the refreshes and give time to the repo-server to catch up. The jitter is the maximum duration that can be
# added to the sync timeout. So, if the sync timeout is 3 minutes and the jitter is 1 minute, then the actual timeout will
# be between 3 and 4 minutes. Disabled when the value is 0, defaults to 0.
timeout.reconciliation.jitter: 0
timeout.reconciliation.jitter: "0"
# cluster.inClusterEnabled indicates whether to allow in-cluster server address. This is enabled by default.
cluster.inClusterEnabled: "true"
# The maximum number of pod logs to render in UI. If the application has more than this number of pods, the logs will not be rendered.
# This is to prevent the UI from becoming unresponsive when rendering a large number of logs. Default is 10.
server.maxPodLogsToRender: 10
server.maxPodLogsToRender: "10"
# Application pod logs RBAC enforcement enables control over who can and who can't view application pod logs.
# When you enable the switch, pod logs will be visible only to admin role by default. Other roles/users will not be able to view them via cli and UI.
@@ -425,7 +425,7 @@ data:
name: some-cluster
server: https://some-cluster
# The maximum size of the payload that can be sent to the webhook server.
webhook.maxPayloadSizeMB: 1024
webhook.maxPayloadSizeMB: "1024"
# application.sync.impersonation.enabled indicates whether the application sync can be decoupled from control plane service account using impersonation.
# application.sync.impersonation.enabled enables application sync to use a custom service account, via impersonation. This allows decoupling sync from control-plane service account.
application.sync.impersonation.enabled: "false"

View File

@@ -98,20 +98,27 @@ data:
return hs
```
In order to prevent duplication of the custom health check for potentially multiple resources, it is also possible to specify a wildcard in the resource kind, and anywhere in the resource group, like this:
In order to prevent duplication of custom health checks for potentially multiple resources, it is also possible to
specify a wildcard in the resource kind, and anywhere in the resource group, like this:
```yaml
resource.customizations.health.ec2.aws.crossplane.io_*: |
...
resource.customizations: |
ec2.aws.crossplane.io/*:
health.lua: |
...
```
```yaml
resource.customizations.health.*.aws.crossplane.io_*: |
...
# If a key _begins_ with a wildcard, please ensure that the GVK key is quoted.
resource.customizations: |
"*.aws.crossplane.io/*":
health.lua: |
...
```
!!!important
Please, note that there can be ambiguous resolution of wildcards, see [#16905](https://github.com/argoproj/argo-cd/issues/16905)
Please, note that wildcards are only supported when using the `resource.customizations` key, the `resource.customizations.health.<group>_<kind>`
style keys do not work since wildcards (`*`) are not supported in Kubernetes configmap keys.
The `obj` is a global variable which contains the resource. The script must return an object with status and optional message field.
The custom health check might return one of the following health statuses:
@@ -121,7 +128,7 @@ The custom health check might return one of the following health statuses:
* `Degraded` - the resource is degraded
* `Suspended` - the resource is suspended and waiting for some external event to resume (e.g. suspended CronJob or paused Deployment)
By default health typically returns `Progressing` status.
By default, health typically returns a `Progressing` status.
NOTE: As a security measure, access to the standard Lua libraries will be disabled by default. Admins can control access by
setting `resource.customizations.useOpenLibs.<group>_<kind>`. In the following example, standard libraries are enabled for health check of `cert-manager.io/Certificate`.

View File

@@ -1,2 +1,5 @@
This page is populated for released Argo CD versions. Use the version selector to view this table for a specific
version.
| Argo CD version | Kubernetes versions |
|-----------------|---------------------|
| 2.13 | |
| 2.12 | |
| 2.11 | v1.29, v1.28, v1.27, v1.26, v1.25 |

View File

@@ -1,5 +1,9 @@
# v2.12 to 2.13
## Upgraded Helm Version
Note that bundled Helm version has been upgraded from 3.15.2 to 3.15.4.
## Custom Resource Actions for Flux Resources
[`Custom Resource Actions`](../resource_actions.md#Custom-Resource-Actions) have been added for Flux Resources.

View File

@@ -68,9 +68,9 @@ This proposal would allow ArgoCD administrators to manage the cluster permission
### Goals
- Applications may only impersonate ServiceAccounts that live in the same namespace as the destination namespace configured in the application.If the service account is created in a different namespace, then the user can provide the service account name in the format `<namespace>:<service_account_name>` . ServiceAccount to be used for syncing each application is determined by the target destination configured in the `AppProject` associated with the `Application`.
- If impersonation feature is enabled, and no service account name is provided in the associated `AppProject`, then the sync operation would fail with an appropriate error message. Users can configure a catch all service account matching all destinations to avoid such sync errors.
- If impersonation feature is enabled, and no service account name is provided in the associated `AppProject`, then the default service account of the destination namespace of the `Application` should be used.
- Access restrictions implemented through properties in AppProject (if done) must have the existing behavior. From a security standpoint, any restrictions that were available before switching to a service account based approach should continue to exist even when the impersonation feature is enabled.
- The feature can be enabled/disabled only at the system level. Once enabled/disabled, it is applicable to all ArgoCD `Applications`.
- The feature can be enabled/disabled only at the system level. Once enabled/disabled, it is applicable to all Argo CD `Applications`.
### Non-Goals
@@ -82,7 +82,7 @@ As part of this proposal, it would be possible for an ArgoCD Admin to specify a
When applications gets synced, based on its destination (target cluster and namespace combination), the `defaultServiceAccount` configured in the `AppProject` will be selected and used for impersonation when executing the kubectl commands for the sync operation.
We would be introducing a new element `destinationServiceAccounts` in `AppProject.spec`. This element is used for the sole purpose of specifying the impersonation configuration. The `defaultServiceAccount` configured for the `AppProject` would be used for the sync operation for a particular destination cluster and namespace. If impersonation feature is enabled and no specific service account is provided in the `AppProject` CR, then the sync operation will fail with an error. Users can configure a catch all service account matching all destinations to avoid such sync errors.
We would be introducing a new element `destinationServiceAccounts` in `AppProject.spec`. This element is used for the sole purpose of specifying the impersonation configuration. The `defaultServiceAccount` configured for the `AppProject` would be used for the sync operation for a particular destination cluster and namespace. If impersonation feature is enabled and no specific service account is provided in the `AppProject` CR, then the `default` service account in the destination namespace would be used for impersonation.
```yaml
apiVersion: argoproj.io/v1alpha1
@@ -109,7 +109,7 @@ spec:
- server: https://kubernetes.default.svc
namespace: guestbook-stage
defaultServiceAccount: guestbook-stage-deployer
- server: '*
- server: '*'
namespace: '*'
defaultServiceAccount: default # catch all service account to be used when all other matches fail.
```
@@ -161,7 +161,10 @@ So that, I can use a generic convention of naming service accounts and avoid ass
#### Component: ArgoCD Application Controller
- Provide a configuration in `argocd-cm` which can be modified to enable the Impersonation feature. Set `application.sync.impersonation.enabled: "true"` in the Argo CD ConfigMap. Default value of `application.sync.impersonation.enabled` would be `"false"` and user has to explicitly override it to use this feature.
- Provide a configuration in `argocd-cm` which can be modified to enable the Impersonation feature. Set `applicationcontroller.enable.impersonation: true` in the Argo CD ConfigMap. Default value of `applicationcontroller.enable.impersonation` would be `false` and user has to explicitly override it to use this feature.
- Provide an option to override the Impersonation feature using environment variables.
Set `ARGOCD_APPLICATION_CONTROLLER_ENABLE_IMPERSONATION=true` in the Application controller environment variables. Default value of the environment variable must be `false` and user has to explicitly set it to `true` to use this feature.
- Provide an option to enable this feature using a command line flag `--enable-impersonation`. This new argument option needs to be added to the Application controller args.
- Fix Application Controller `sync.go` to set the Impersonate configuration from the AppProject CR to the `SyncContext` Object (rawConfig and restConfig field, need to understand which config is used for the actual sync and if both configs need to be impersonated.)
#### Component: ArgoCD UI

View File

@@ -30,6 +30,7 @@ argocd admin proj generate-spec PROJECT [flags]
--deny-namespaced-resource stringArray List of denied namespaced resources
--description string Project description
-d, --dest stringArray Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)
--dest-service-accounts stringArray Destination server, namespace and target service account (e.g. https://192.168.99.100:8443,default,default-sa)
-f, --file string Filename or URL to Kubernetes manifests for the project
-h, --help help for generate-spec
-i, --inline If set then generated resource is written back to the file specified in --file flag

View File

@@ -27,6 +27,7 @@ argocd proj create PROJECT [flags]
--deny-namespaced-resource stringArray List of denied namespaced resources
--description string Project description
-d, --dest stringArray Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)
--dest-service-accounts stringArray Destination server, namespace and target service account (e.g. https://192.168.99.100:8443,default,default-sa)
-f, --file string Filename or URL to Kubernetes manifests for the project
-h, --help help for create
--orphaned-resources Enables orphaned resources monitoring

View File

@@ -27,6 +27,7 @@ argocd proj set PROJECT [flags]
--deny-namespaced-resource stringArray List of denied namespaced resources
--description string Project description
-d, --dest stringArray Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)
--dest-service-accounts stringArray Destination server, namespace and target service account (e.g. https://192.168.99.100:8443,default,default-sa)
-h, --help help for set
--orphaned-resources Enables orphaned resources monitoring
--orphaned-resources-warn Specifies if applications should have a warning condition when orphaned resources detected

View File

@@ -0,0 +1 @@
1bc3f354f7ce4d7fd9cfa5bcc701c1f32c88d27076d96c2792d5b5226062aee5 helm-v3.15.4-darwin-amd64.tar.gz

View File

@@ -0,0 +1 @@
88115846a1fb58f8eb8f64fec5c343d95ca394f1be811602fa54a887c98730ac helm-v3.15.4-darwin-arm64.tar.gz

View File

@@ -0,0 +1 @@
11400fecfc07fd6f034863e4e0c4c4445594673fd2a129e701fe41f31170cfa9 helm-v3.15.4-linux-amd64.tar.gz

View File

@@ -0,0 +1 @@
fa419ecb139442e8a594c242343fafb7a46af3af34041c4eac1efcc49d74e626 helm-v3.15.4-linux-arm64.tar.gz

View File

@@ -0,0 +1 @@
e4efce93723f52dd858e9046ea836c9c75f346facce1b87b8cf78c817b97e6ac helm-v3.15.4-linux-ppc64le.tar.gz

View File

@@ -0,0 +1 @@
c6e0cdea598196895ac7b627ce972699ef9f06b0eba51dc4db7cc21b3369f24a helm-v3.15.4-linux-s390x.tar.gz

View File

@@ -11,7 +11,7 @@
# Use ./hack/installers/checksums/add-helm-checksums.sh and
# add-kustomize-checksums.sh to help download checksums.
###############################################################################
helm3_version=3.15.2
helm3_version=3.15.4
kubectl_version=1.17.8
kubectx_version=0.6.3
kustomize5_version=5.4.3

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.13.0-rc3
resources:
- ./application-controller
- ./dex

View File

@@ -15,6 +15,7 @@ rules:
- delete # supports deletion a live object in UI
- get # supports viewing live object manifest in UI
- patch # supports `argocd app patch`
- list # supports `argocd appset generate` with cluster generator
- apiGroups:
- ""
resources:

View File

@@ -21735,7 +21735,7 @@ spec:
sync operation.
properties:
defaultServiceAccount:
description: ServiceAccountName to be used for impersonation
description: DefaultServiceAccount to be used for impersonation
during the sync operation
type: string
namespace:
@@ -21746,6 +21746,9 @@ spec:
description: Server specifies the URL of the target cluster's
Kubernetes control plane API.
type: string
required:
- defaultServiceAccount
- server
type: object
type: array
destinations:
@@ -22558,7 +22561,7 @@ spec:
key: applicationsetcontroller.webhook.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -22676,7 +22679,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -22929,7 +22932,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -22981,7 +22984,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -23253,7 +23256,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.13.0-rc3

View File

@@ -95,7 +95,7 @@ spec:
sync operation.
properties:
defaultServiceAccount:
description: ServiceAccountName to be used for impersonation
description: DefaultServiceAccount to be used for impersonation
during the sync operation
type: string
namespace:
@@ -106,6 +106,9 @@ spec:
description: Server specifies the URL of the target cluster's
Kubernetes control plane API.
type: string
required:
- defaultServiceAccount
- server
type: object
type: array
destinations:

View File

@@ -12,7 +12,7 @@ patches:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.13.0-rc3
resources:
- ../../base/application-controller
- ../../base/applicationset-controller

View File

@@ -21735,7 +21735,7 @@ spec:
sync operation.
properties:
defaultServiceAccount:
description: ServiceAccountName to be used for impersonation
description: DefaultServiceAccount to be used for impersonation
during the sync operation
type: string
namespace:
@@ -21746,6 +21746,9 @@ spec:
description: Server specifies the URL of the target cluster's
Kubernetes control plane API.
type: string
required:
- defaultServiceAccount
- server
type: object
type: array
destinations:
@@ -22463,6 +22466,7 @@ rules:
- delete
- get
- patch
- list
- apiGroups:
- ""
resources:
@@ -23901,7 +23905,7 @@ spec:
key: applicationsetcontroller.webhook.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -24036,7 +24040,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -24124,7 +24128,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -24243,7 +24247,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -24524,7 +24528,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -24576,7 +24580,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -24930,7 +24934,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -25238,7 +25242,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -1694,7 +1694,7 @@ spec:
key: applicationsetcontroller.webhook.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1829,7 +1829,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1917,7 +1917,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2036,7 +2036,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2317,7 +2317,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2369,7 +2369,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2723,7 +2723,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3031,7 +3031,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-application-controller
ports:

22
manifests/install.yaml generated
View File

@@ -21735,7 +21735,7 @@ spec:
sync operation.
properties:
defaultServiceAccount:
description: ServiceAccountName to be used for impersonation
description: DefaultServiceAccount to be used for impersonation
during the sync operation
type: string
namespace:
@@ -21746,6 +21746,9 @@ spec:
description: Server specifies the URL of the target cluster's
Kubernetes control plane API.
type: string
required:
- defaultServiceAccount
- server
type: object
type: array
destinations:
@@ -22430,6 +22433,7 @@ rules:
- delete
- get
- patch
- list
- apiGroups:
- ""
resources:
@@ -23018,7 +23022,7 @@ spec:
key: applicationsetcontroller.webhook.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -23153,7 +23157,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -23241,7 +23245,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -23341,7 +23345,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -23594,7 +23598,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -23646,7 +23650,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -23998,7 +24002,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -24306,7 +24310,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -811,7 +811,7 @@ spec:
key: applicationsetcontroller.webhook.parallelism.limit
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -946,7 +946,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1034,7 +1034,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1134,7 +1134,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1387,7 +1387,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1439,7 +1439,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1791,7 +1791,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2099,7 +2099,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.13.0-rc3
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -6,15 +6,22 @@ import (
"strconv"
"strings"
"github.com/argoproj/argo-cd/v2/util/git"
"github.com/argoproj/argo-cd/v2/util/glob"
globutil "github.com/gobwas/glob"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/argoproj/argo-cd/v2/util/git"
"github.com/argoproj/argo-cd/v2/util/glob"
)
const (
// serviceAccountDisallowedCharSet contains the characters that are not allowed to be present
// in a DefaultServiceAccount configured for a DestinationServiceAccount
serviceAccountDisallowedCharSet = "!*[]{}\\/"
)
type ErrApplicationNotAllowedToUseProject struct {
@@ -267,12 +274,27 @@ func (p *AppProject) ValidateProject() error {
destServiceAccts := make(map[string]bool)
for _, destServiceAcct := range p.Spec.DestinationServiceAccounts {
if destServiceAcct.Server == "!*" {
return status.Errorf(codes.InvalidArgument, "server has an invalid format, '!*'")
if strings.Contains(destServiceAcct.Server, "!") {
return status.Errorf(codes.InvalidArgument, "server has an invalid format, '%s'", destServiceAcct.Server)
}
if destServiceAcct.Namespace == "!*" {
return status.Errorf(codes.InvalidArgument, "namespace has an invalid format, '!*'")
if strings.Contains(destServiceAcct.Namespace, "!") {
return status.Errorf(codes.InvalidArgument, "namespace has an invalid format, '%s'", destServiceAcct.Namespace)
}
if strings.Trim(destServiceAcct.DefaultServiceAccount, " ") == "" ||
strings.ContainsAny(destServiceAcct.DefaultServiceAccount, serviceAccountDisallowedCharSet) {
return status.Errorf(codes.InvalidArgument, "defaultServiceAccount has an invalid format, '%s'", destServiceAcct.DefaultServiceAccount)
}
_, err := globutil.Compile(destServiceAcct.Server)
if err != nil {
return status.Errorf(codes.InvalidArgument, "server has an invalid format, '%s'", destServiceAcct.Server)
}
_, err = globutil.Compile(destServiceAcct.Namespace)
if err != nil {
return status.Errorf(codes.InvalidArgument, "namespace has an invalid format, '%s'", destServiceAcct.Namespace)
}
key := fmt.Sprintf("%s/%s", destServiceAcct.Server, destServiceAcct.Namespace)

View File

@@ -156,7 +156,7 @@ message ApplicationDestinationServiceAccount {
// Namespace specifies the target namespace for the application's resources.
optional string namespace = 2;
// ServiceAccountName to be used for impersonation during the sync operation
// DefaultServiceAccount to be used for impersonation during the sync operation
optional string defaultServiceAccount = 3;
}

View File

@@ -2767,11 +2767,11 @@ type KustomizeOptions struct {
// ApplicationDestinationServiceAccount holds information about the service account to be impersonated for the application sync operation.
type ApplicationDestinationServiceAccount struct {
// Server specifies the URL of the target cluster's Kubernetes control plane API.
Server string `json:"server,omitempty" protobuf:"bytes,1,opt,name=server"`
Server string `json:"server" protobuf:"bytes,1,opt,name=server"`
// Namespace specifies the target namespace for the application's resources.
Namespace string `json:"namespace,omitempty" protobuf:"bytes,2,opt,name=namespace"`
// ServiceAccountName to be used for impersonation during the sync operation
DefaultServiceAccount string `json:"defaultServiceAccount,omitempty" protobuf:"bytes,3,opt,name=defaultServiceAccount"`
// DefaultServiceAccount to be used for impersonation during the sync operation
DefaultServiceAccount string `json:"defaultServiceAccount" protobuf:"bytes,3,opt,name=defaultServiceAccount"`
}
// CascadedDeletion indicates if the deletion finalizer is set and controller should delete the application and it's cascaded resources

View File

@@ -3959,3 +3959,158 @@ func TestApplicationTree_Merge(t *testing.T) {
},
}, tree)
}
func TestAppProject_ValidateDestinationServiceAccount(t *testing.T) {
testData := []struct {
server string
namespace string
defaultServiceAccount string
expectedErrMsg string
}{
{
// Given, a project
// When, a default destination service account with all valid fields is added to it,
// Then, there is no error.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: "test-sa",
expectedErrMsg: "",
},
{
// Given, a project
// When, a default destination service account with negation glob pattern for server is added,
// Then, there is an error with appropriate message.
server: "!abc",
namespace: "test-ns",
defaultServiceAccount: "test-sa",
expectedErrMsg: "server has an invalid format, '!abc'",
},
{
// Given, a project
// When, a default destination service account with empty namespace is added to it,
// Then, there is no error.
server: "https://192.168.99.100:8443",
namespace: "",
defaultServiceAccount: "test-sa",
expectedErrMsg: "",
},
{
// Given, a project,
// When, a default destination service account with negation glob pattern for server is added,
// Then, there is an error with appropriate message.
server: "!*",
namespace: "test-ns",
defaultServiceAccount: "test-sa",
expectedErrMsg: "server has an invalid format, '!*'",
},
{
// Given, a project,
// When, a default destination service account with negation glob pattern for namespace is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "!*",
defaultServiceAccount: "test-sa",
expectedErrMsg: "namespace has an invalid format, '!*'",
},
{
// Given, a project,
// When, a default destination service account with negation glob pattern for namespace is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "!abc",
defaultServiceAccount: "test-sa",
expectedErrMsg: "namespace has an invalid format, '!abc'",
},
{
// Given, a project,
// When, a default destination service account with empty service account is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: "",
expectedErrMsg: "defaultServiceAccount has an invalid format, ''",
},
{
// Given, a project,
// When, a default destination service account with service account having just white spaces is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: " ",
expectedErrMsg: "defaultServiceAccount has an invalid format, ' '",
},
{
// Given, a project,
// When, a default destination service account with service account having backwards slash char is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: "test\\sa",
expectedErrMsg: "defaultServiceAccount has an invalid format, 'test\\sa'",
},
{
// Given, a project,
// When, a default destination service account with service account having forward slash char is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: "test/sa",
expectedErrMsg: "defaultServiceAccount has an invalid format, 'test/sa'",
},
{
// Given, a project,
// When, a default destination service account with service account having square braces char is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: "[test-sa]",
expectedErrMsg: "defaultServiceAccount has an invalid format, '[test-sa]'",
},
{
// Given, a project,
// When, a default destination service account with service account having curly braces char is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "test-ns",
defaultServiceAccount: "{test-sa}",
expectedErrMsg: "defaultServiceAccount has an invalid format, '{test-sa}'",
},
{
// Given, a project,
// When, a default destination service account with service account having curly braces char is added,
// Then, there is an error with appropriate message.
server: "[[ech*",
namespace: "test-ns",
defaultServiceAccount: "test-sa",
expectedErrMsg: "server has an invalid format, '[[ech*'",
},
{
// Given, a project,
// When, a default destination service account with service account having curly braces char is added,
// Then, there is an error with appropriate message.
server: "https://192.168.99.100:8443",
namespace: "[[ech*",
defaultServiceAccount: "test-sa",
expectedErrMsg: "namespace has an invalid format, '[[ech*'",
},
}
for _, data := range testData {
proj := AppProject{
Spec: AppProjectSpec{
DestinationServiceAccounts: []ApplicationDestinationServiceAccount{
{
Server: data.server,
Namespace: data.namespace,
DefaultServiceAccount: data.defaultServiceAccount,
},
},
},
}
err := proj.ValidateProject()
if data.expectedErrMsg == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, data.expectedErrMsg)
}
}
}

View File

@@ -14,10 +14,10 @@ if obj.status.status ~= nil then
-- "root" policy
for i, entry in ipairs(obj.status.status) do
if entry.compliant ~= "Compliant" then
noncompliants[i] = entry.clustername
table.insert(noncompliants, entry.clustername)
end
end
if table.getn(noncompliants) == 0 then
if #noncompliants == 0 then
hs.message = "All clusters are compliant"
else
hs.message = "NonCompliant clusters: " .. table.concat(noncompliants, ", ")
@@ -26,10 +26,10 @@ elseif obj.status.details ~= nil then
-- "replicated" policy
for i, entry in ipairs(obj.status.details) do
if entry.compliant ~= "Compliant" then
noncompliants[i] = entry.templateMeta.name
table.insert(noncompliants, entry.templateMeta.name)
end
end
if table.getn(noncompliants) == 0 then
if #noncompliants == 0 then
hs.message = "All templates are compliant"
else
hs.message = "NonCompliant templates: " .. table.concat(noncompliants, ", ")

View File

@@ -0,0 +1,88 @@
apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
name: open-cluster-management-global-set.argo-example
namespace: local-cluster
labels:
policy.open-cluster-management.io/cluster-name: local-cluster
policy.open-cluster-management.io/cluster-namespace: local-cluster
policy.open-cluster-management.io/root-policy: open-cluster-management-global-set.argo-example
spec:
disabled: false
policy-templates:
- objectDefinition:
apiVersion: policy.open-cluster-management.io/v1
kind: ConfigurationPolicy
metadata:
name: example-namespace
spec:
object-templates:
- complianceType: musthave
objectDefinition:
apiVersion: v1
kind: Namespace
metadata:
name: example
remediationAction: inform
severity: low
- objectDefinition:
apiVersion: policy.open-cluster-management.io/v1
kind: ConfigurationPolicy
metadata:
name: example-pod
spec:
namespaceSelector:
exclude:
- kube-*
include:
- default
object-templates:
- complianceType: musthave
objectDefinition:
apiVersion: v1
kind: Pod
metadata:
name: foobar
spec:
containers:
- image: 'registry.redhat.io/rhel9/httpd-24:latest'
name: httpd
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
runAsNonRoot: true
remediationAction: enforce
severity: low
status:
compliant: NonCompliant
details:
- compliant: Compliant
history:
- eventName: open-cluster-management-global-set.argo-example.17e7034c879045a3
lastTimestamp: '2024-07-30T14:16:49Z'
message: 'Compliant; notification - pods [foobar] was created successfully in namespace default'
templateMeta:
creationTimestamp: null
name: example-foo
- compliant: NonCompliant
history:
- eventName: open-cluster-management-global-set.argo-example.17e701cc5101e3a4
lastTimestamp: '2024-07-30T13:49:19Z'
message: 'NonCompliant; violation - namespaces [example] not found'
templateMeta:
creationTimestamp: null
name: example-namespace
- compliant: Compliant
history:
- eventName: open-cluster-management-global-set.argo-example.17e7034c879045a3
lastTimestamp: '2024-07-30T14:16:49Z'
message: 'Compliant; notification - pods [foobar] was created successfully in namespace default'
- eventName: open-cluster-management-global-set.argo-example.17e7020b47782ddc
lastTimestamp: '2024-07-30T13:53:49Z'
message: 'NonCompliant; violation - pods [foobar] not found in namespace default'
templateMeta:
creationTimestamp: null
name: example-pod

View File

@@ -1,50 +1,59 @@
-- isInferenceServiceInRawDeploymentMode determines if the inference service deployed in RawDeployment mode
-- KServe v12 and above supports Rawdeployment for Inference graphs. For Inference services, KServe has supported RawDeployment model since [v0.7.0](https://github.com/kserve/kserve/releases/tag/v0.7.0).
function isInferenceServiceInRawDeploymentMode(obj)
if obj.metadata.annotations == nil then
return false
end
local deploymentMode = obj.metadata.annotations["serving.kserve.io/deploymentMode"]
return deploymentMode ~= nil and deploymentMode == "RawDeployment"
end
local health_status = {}
health_status.status = "Progressing"
health_status.message = "Waiting for status update."
if obj.status ~= nil and obj.status.conditions ~= nil then
local status_true = 0
health_status.message = "Waiting for InferenceService to report status..."
if obj.status ~= nil then
local progressing = false
local degraded = false
local status_false = 0
local status_unknown = 0
health_status.message = ""
for i, condition in pairs(obj.status.conditions) do
if condition.status == "True" and (condition.type == "IngressReady" or condition.type == "PredictorConfigurationReady" or condition.type == "PredictorReady" or condition.type == "PredictorRouteReady" or condition.type == "Ready") then
status_true = status_true + 1
elseif condition.status == "False" or condition.status == "Unknown" then
msg = condition.type .. " is " .. condition.status
if condition.reason ~= nil and condition.reason ~= "" then
msg = msg .. ", since " .. condition.reason .. "."
end
if condition.message ~= nil and condition.message ~= "" then
msg = msg .. " " .. condition.message
end
health_status.message = health_status.message .. msg .. "\n"
if condition.status == "False" then
status_false = status_false + 1
local msg = ""
if obj.status.modelStatus ~= nil then
if obj.status.modelStatus.transitionStatus ~= "UpToDate" then
if obj.status.modelStatus.transitionStatus == "InProgress" then
progressing = true
else
status_unknown = status_unknown + 1
degraded = true
end
msg = msg .. "0: transitionStatus | " .. obj.status.modelStatus.transitionStatus
end
end
if ((isInferenceServiceInRawDeploymentMode(obj) and status_true == 3) or status_true == 5) and status_false == 0 and status_unknown == 0 then
health_status.message = "Inference Service is healthy."
health_status.status = "Healthy"
return health_status
elseif status_false > 0 then
health_status.status = "Degraded"
return health_status
else
health_status.status = "Progressing"
return health_status
if obj.status.conditions ~= nil then
for i, condition in pairs(obj.status.conditions) do
if condition.status == "Unknown" then
status_unknown = status_unknown + 1
elseif condition.status == "False" then
status_false = status_false + 1
end
if condition.status ~= "True" then
msg = msg .. " | " .. i .. ": " .. condition.type .. " | " .. condition.status
if condition.reason ~= nil and condition.reason ~= "" then
msg = msg .. " | " .. condition.reason
end
if condition.message ~= nil and condition.message ~= "" then
msg = msg .. " | " .. condition.message
end
end
end
if progressing == false and degraded == false and status_unknown == 0 and status_false == 0 then
health_status.status = "Healthy"
msg = "InferenceService is healthy."
elseif degraded == false and status_unknown >= 0 then
health_status.status = "Progressing"
else
health_status.status = "Degraded"
end
health_status.message = msg
end
end
return health_status
return health_status

View File

@@ -1,17 +1,41 @@
tests:
- healthStatus:
status: Progressing
message: "PredictorConfigurationReady is Unknown\nPredictorReady is Unknown, since RevisionMissing. Configuration \"hello-world-predictor-default\" is waiting for a Revision to become ready.\nPredictorRouteReady is Unknown, since RevisionMissing. Configuration \"hello-world-predictor-default\" is waiting for a Revision to become ready.\nReady is Unknown, since RevisionMissing. Configuration \"hello-world-predictor-default\" is waiting for a Revision to become ready.\n"
message: ' | 1: PredictorConfigurationReady | Unknown | 2: PredictorReady | Unknown | RevisionMissing | Configuration "hello-world-predictor-default" is waiting for a Revision to become ready. | 3: PredictorRouteReady | Unknown | RevisionMissing | Configuration "hello-world-predictor-default" is waiting for a Revision to become ready. | 4: Ready | Unknown | RevisionMissing | Configuration "hello-world-predictor-default" is waiting for a Revision to become ready.'
inputPath: testdata/progressing.yaml
- healthStatus:
status: Progressing
message: '0: transitionStatus | InProgress | 1: LatestDeploymentReady | Unknown | PredictorConfigurationReady not ready | 2: PredictorConfigurationReady | Unknown | 3: PredictorReady | Unknown | RevisionMissing | Configuration "helloworld-predictor" is waiting for a Revision to become ready. | 4: PredictorRouteReady | Unknown | RevisionMissing | Configuration "helloworld-predictor" is waiting for a Revision to become ready. | 5: Ready | Unknown | RevisionMissing | Configuration "helloworld-predictor" is waiting for a Revision to become ready. | 6: RoutesReady | Unknown | PredictorRouteReady not ready'
inputPath: testdata/progressing_ocp.yaml
- healthStatus:
status: Progressing
message: "0: transitionStatus | InProgress | 1: PredictorReady | False | 2: Ready | False"
inputPath: testdata/progressing_modelmesh.yaml
- healthStatus:
status: Degraded
message: "IngressReady is False, since Predictor ingress not created.\nPredictorConfigurationReady is False, since RevisionFailed. Revision \"helloworld-00002\" failed with message: Container failed with: container exited with no error.\nPredictorReady is False, since RevisionFailed. Revision \"helloworld-00002\" failed with message: Container failed with: container exited with no error.\nReady is False, since Predictor ingress not created.\n"
message: '0: transitionStatus | BlockedByFailedLoad | 1: IngressReady | False | Predictor ingress not created | 2: PredictorConfigurationReady | False | RevisionFailed | Revision "helloworld-00002" failed with message: Container failed with: container exited with no error. | 3: PredictorReady | False | RevisionFailed | Revision "helloworld-00002" failed with message: Container failed with: container exited with no error. | 5: Ready | False | Predictor ingress not created'
inputPath: testdata/degraded.yaml
- healthStatus:
status: Degraded
message: '0: transitionStatus | BlockedByFailedLoad | 1: LatestDeploymentReady | False | PredictorConfigurationReady not ready | 2: PredictorConfigurationReady | False | RevisionFailed | Revision "helloworld-predictor-00002" failed with message: . | 3: PredictorReady | False | RevisionMissing | Configuration "helloworld-predictor" does not have any ready Revision. | 4: PredictorRouteReady | False | RevisionMissing | Configuration "helloworld-predictor" does not have any ready Revision. | 5: Ready | False | RevisionMissing | Configuration "helloworld-predictor" does not have any ready Revision. | 6: RoutesReady | False | PredictorRouteReady not ready'
inputPath: testdata/degraded_ocp.yaml
- healthStatus:
status: Degraded
message: "0: transitionStatus | BlockedByFailedLoad"
inputPath: testdata/degraded_modelmesh.yaml
- healthStatus:
status: Healthy
message: Inference Service is healthy.
message: InferenceService is healthy.
inputPath: testdata/healthy.yaml
- healthStatus:
status: Healthy
message: Inference Service is healthy.
message: InferenceService is healthy.
inputPath: testdata/healthy_ocp.yaml
- healthStatus:
status: Healthy
message: InferenceService is healthy.
inputPath: testdata/healthy_modelmesh.yaml
- healthStatus:
status: Healthy
message: InferenceService is healthy.
inputPath: testdata/healthy_raw.yaml

View File

@@ -28,3 +28,5 @@ status:
reason: Predictor ingress not created
status: "False"
type: Ready
modelStatus:
transitionStatus: BlockedByFailedLoad

View File

@@ -0,0 +1,16 @@
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: helloworld
namespace: default
spec: {}
status:
conditions:
- lastTransitionTime: '2024-05-30T22:43:16Z'
status: 'True'
type: PredictorReady
- lastTransitionTime: '2024-05-30T22:43:16Z'
status: 'True'
type: Ready
modelStatus:
transitionStatus: BlockedByFailedLoad

View File

@@ -0,0 +1,42 @@
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: helloworld
namespace: default
spec: {}
status:
conditions:
- lastTransitionTime: '2024-05-30T23:03:45Z'
reason: PredictorConfigurationReady not ready
severity: Info
status: 'False'
type: LatestDeploymentReady
- lastTransitionTime: '2024-05-30T23:03:45Z'
message: 'Revision "helloworld-predictor-00002" failed with message: .'
reason: RevisionFailed
severity: Info
status: 'False'
type: PredictorConfigurationReady
- lastTransitionTime: '2024-05-30T23:03:45Z'
message: Configuration "helloworld-predictor" does not have any ready Revision.
reason: RevisionMissing
status: 'False'
type: PredictorReady
- lastTransitionTime: '2024-05-30T23:03:45Z'
message: Configuration "helloworld-predictor" does not have any ready Revision.
reason: RevisionMissing
severity: Info
status: 'False'
type: PredictorRouteReady
- lastTransitionTime: '2024-05-30T23:03:45Z'
message: Configuration "helloworld-predictor" does not have any ready Revision.
reason: RevisionMissing
status: 'False'
type: Ready
- lastTransitionTime: '2024-05-30T23:03:45Z'
reason: PredictorRouteReady not ready
severity: Info
status: 'False'
type: RoutesReady
modelStatus:
transitionStatus: BlockedByFailedLoad

View File

@@ -0,0 +1,16 @@
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: helloworld
namespace: default
spec: {}
status:
conditions:
- lastTransitionTime: '2024-05-30T22:43:16Z'
status: 'True'
type: PredictorReady
- lastTransitionTime: '2024-05-30T22:43:16Z'
status: 'True'
type: Ready
modelStatus:
transitionStatus: UpToDate

View File

@@ -0,0 +1,35 @@
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: helloworld
namespace: default
spec: {}
status:
conditions:
- lastTransitionTime: '2024-05-30T22:14:31Z'
status: 'True'
type: IngressReady
- lastTransitionTime: '2024-05-30T22:14:30Z'
severity: Info
status: 'True'
type: LatestDeploymentReady
- lastTransitionTime: '2024-05-30T22:14:30Z'
severity: Info
status: 'True'
type: PredictorConfigurationReady
- lastTransitionTime: '2024-05-30T22:14:31Z'
status: 'True'
type: PredictorReady
- lastTransitionTime: '2024-05-30T22:14:31Z'
severity: Info
status: 'True'
type: PredictorRouteReady
- lastTransitionTime: '2024-05-30T22:14:31Z'
status: 'True'
type: Ready
- lastTransitionTime: '2024-05-30T22:14:31Z'
severity: Info
status: 'True'
type: RoutesReady
modelStatus:
transitionStatus: UpToDate

View File

@@ -0,0 +1,16 @@
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: helloworld
namespace: default
spec: {}
status:
conditions:
- lastTransitionTime: '2024-05-30T22:43:16Z'
status: 'False'
type: PredictorReady
- lastTransitionTime: '2024-05-30T22:43:16Z'
status: 'False'
type: Ready
modelStatus:
transitionStatus: InProgress

View File

@@ -0,0 +1,40 @@
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: helloworld
namespace: default
spec: {}
status:
conditions:
- lastTransitionTime: '2024-05-30T22:29:46Z'
reason: PredictorConfigurationReady not ready
severity: Info
status: Unknown
type: LatestDeploymentReady
- lastTransitionTime: '2024-05-30T22:29:46Z'
severity: Info
status: Unknown
type: PredictorConfigurationReady
- lastTransitionTime: '2024-05-30T22:29:46Z'
message: Configuration "helloworld-predictor" is waiting for a Revision to become ready.
reason: RevisionMissing
status: Unknown
type: PredictorReady
- lastTransitionTime: '2024-05-30T22:29:46Z'
message: Configuration "helloworld-predictor" is waiting for a Revision to become ready.
reason: RevisionMissing
severity: Info
status: Unknown
type: PredictorRouteReady
- lastTransitionTime: '2024-05-30T22:29:46Z'
message: Configuration "helloworld-predictor" is waiting for a Revision to become ready.
reason: RevisionMissing
status: Unknown
type: Ready
- lastTransitionTime: '2024-05-30T22:29:46Z'
reason: PredictorRouteReady not ready
severity: Info
status: Unknown
type: RoutesReady
modelStatus:
transitionStatus: InProgress

View File

@@ -33,6 +33,12 @@ const (
DefaultIdleConnectionTimeout = 60 * time.Second
DefaultMaxIdleConnections = 30
// HeaderArgoCDNamespace defines the namespace of the
// argo control plane to be passed to the extension handler.
// Example:
// Argocd-Namespace: "namespace"
HeaderArgoCDNamespace = "Argocd-Namespace"
// HeaderArgoCDApplicationName defines the name of the
// expected application header to be passed to the extension
// handler. The header value must follow the format:
@@ -333,6 +339,7 @@ type RbacEnforcer interface {
// and handling proxy extensions.
type Manager struct {
log *log.Entry
namespace string
settings SettingsGetter
application ApplicationGetter
project ProjectGetter
@@ -355,9 +362,10 @@ type ExtensionMetricsRegistry interface {
}
// NewManager will initialize a new manager.
func NewManager(log *log.Entry, sg SettingsGetter, ag ApplicationGetter, pg ProjectGetter, rbac RbacEnforcer, ug UserGetter) *Manager {
func NewManager(log *log.Entry, namespace string, sg SettingsGetter, ag ApplicationGetter, pg ProjectGetter, rbac RbacEnforcer, ug UserGetter) *Manager {
return &Manager{
log: log,
namespace: namespace,
settings: sg,
application: ag,
project: pg,
@@ -740,7 +748,7 @@ func (m *Manager) CallExtension() func(http.ResponseWriter, *http.Request) {
user := m.userGetter.GetUser(r.Context())
groups := m.userGetter.GetGroups(r.Context())
prepareRequest(r, extName, app, user, groups)
prepareRequest(r, m.namespace, extName, app, user, groups)
m.log.Debugf("proxing request for extension %q", extName)
// httpsnoop package is used to properly wrap the responseWriter
// and avoid optional intefaces issue:
@@ -763,11 +771,13 @@ func registerMetrics(extName string, metrics httpsnoop.Metrics, extensionMetrics
// the Argo CD extension API section from it. It provides additional information to
// the backend service appending them in the outgoing request headers. The appended
// headers are:
// - Control plane namespace
// - Cluster destination name
// - Cluster destination server
// - Argo CD authenticated username
func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application, username string, groups []string) {
func prepareRequest(r *http.Request, namespace string, extName string, app *v1alpha1.Application, username string, groups []string) {
r.URL.Path = strings.TrimPrefix(r.URL.Path, fmt.Sprintf("%s/%s", URLPrefix, extName))
r.Header.Set(HeaderArgoCDNamespace, namespace)
if app.Spec.Destination.Name != "" {
r.Header.Set(HeaderArgoCDTargetClusterName, app.Spec.Destination.Name)
}

View File

@@ -150,7 +150,7 @@ func TestRegisterExtensions(t *testing.T) {
logger, _ := test.NewNullLogger()
logEntry := logger.WithContext(context.Background())
m := extension.NewManager(logEntry, settMock, nil, nil, nil, nil)
m := extension.NewManager(logEntry, "", settMock, nil, nil, nil, nil)
return &fixture{
settingsGetterMock: settMock,
@@ -248,6 +248,7 @@ func TestCallExtension(t *testing.T) {
userMock *mocks.UserGetter
manager *extension.Manager
}
defaultServerNamespace := "control-plane-ns"
defaultProjectName := "project-name"
setup := func() *fixture {
@@ -260,7 +261,7 @@ func TestCallExtension(t *testing.T) {
logger, _ := test.NewNullLogger()
logEntry := logger.WithContext(context.Background())
m := extension.NewManager(logEntry, settMock, appMock, projMock, rbacMock, userMock)
m := extension.NewManager(logEntry, defaultServerNamespace, settMock, appMock, projMock, rbacMock, userMock)
m.AddMetricsRegistry(metricsMock)
mux := http.NewServeMux()
@@ -444,6 +445,7 @@ func TestCallExtension(t *testing.T) {
require.NoError(t, err)
actual := strings.TrimSuffix(string(body), "\n")
assert.Equal(t, backendResponse, actual)
assert.Equal(t, defaultServerNamespace, resp.Header.Get(extension.HeaderArgoCDNamespace))
assert.Equal(t, clusterURL, resp.Header.Get(extension.HeaderArgoCDTargetClusterURL))
assert.Equal(t, "Bearer some-bearer-token", resp.Header.Get("Authorization"))
assert.Equal(t, "some-user", resp.Header.Get(extension.HeaderArgoCDUsername))

View File

@@ -327,7 +327,7 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio
ag := extension.NewDefaultApplicationGetter(appLister)
pg := extension.NewDefaultProjectGetter(projLister, dbInstance)
ug := extension.NewDefaultUserGetter(policyEnf)
em := extension.NewManager(logger, sg, ag, pg, enf, ug)
em := extension.NewManager(logger, opts.Namespace, sg, ag, pg, enf, ug)
a := &ArgoCDServer{
ArgoCDServerOpts: opts,

View File

@@ -42,6 +42,31 @@ func (c *Consequences) Expect(e Expectation) *Consequences {
return c
}
// ExpectConsistently will continuously evaluate a condition, and it must be true each time it is evaluated, otherwise the test is failed. The condition will be repeatedly evaluated until 'expirationDuration' is met, waiting 'waitDuration' after each success.
func (c *Consequences) ExpectConsistently(e Expectation, waitDuration time.Duration, expirationDuration time.Duration) *Consequences {
// this invocation makes sure this func is not reported as the cause of the failure - we are a "test helper"
c.context.t.Helper()
expiration := time.Now().Add(expirationDuration)
for time.Now().Before(expiration) {
state, message := e(c)
switch state {
case succeeded:
log.Infof("expectation succeeded: %s", message)
case failed:
c.context.t.Fatalf("failed expectation: %s", message)
return c
}
// On condition success: wait, then retry
log.Infof("Expectation '%s' passes, repeating to ensure consistency", message)
time.Sleep(waitDuration)
}
// If the condition never failed before expiring, it is a pass.
return c
}
func (c *Consequences) And(block func(app *Application)) *Consequences {
c.context.t.Helper()
block(c.app())

View File

@@ -619,3 +619,225 @@ func TestGetVirtualProjectMatch(t *testing.T) {
_, err = fixture.RunCli("app", "sync", fixture.Name(), "--resource", ":Service:guestbook-ui", "--timeout", fmt.Sprintf("%v", 10))
assert.Contains(t, err.Error(), "blocked by sync window")
}
func TestAddProjectDestinationServiceAccount(t *testing.T) {
fixture.EnsureCleanState(t)
projectName := "proj-" + strconv.FormatInt(time.Now().Unix(), 10)
_, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Create(
context.Background(), &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: projectName}}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unable to create project %v", err)
}
// Given, an existing project
// When, a default destination service account with all valid fields is added to it,
// Then, there is no error.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"test-sa",
)
if err != nil {
t.Fatalf("Unable to add project destination service account %v", err)
}
// Given, an existing project
// When, a default destination service account with empty namespace is added to it,
// Then, there is no error.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"",
"test-sa",
)
if err != nil {
t.Fatalf("Unable to add project destination service account %v", err)
}
// Given, an existing project,
// When, a default destination service account is added with a custom service account namespace,
// Then, there is no error.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns1",
"test-sa",
"--service-account-namespace",
"default",
)
if err != nil {
t.Fatalf("Unable to add project destination service account %v", err)
}
// Given, an existing project,
// When, a duplicate default destination service account is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "already defined")
// Given, an existing project,
// When, a duplicate default destination service account is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"asdf",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "already added")
// Given, an existing project,
// When, a default destination service account with negation glob pattern for server is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"!*",
"test-ns",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "server has an invalid format, '!*'")
// Given, an existing project,
// When, a default destination service account with negation glob pattern for server is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"!abc",
"test-ns",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "server has an invalid format, '!abc'")
// Given, an existing project,
// When, a default destination service account with negation glob pattern for namespace is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"!*",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "namespace has an invalid format, '!*'")
// Given, an existing project,
// When, a default destination service account with negation glob pattern for namespace is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"!abc",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "namespace has an invalid format, '!abc'")
// Given, an existing project,
// When, a default destination service account with empty service account is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "defaultServiceAccount has an invalid format, ''")
// Given, an existing project,
// When, a default destination service account with service account having just white spaces is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
" ",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "defaultServiceAccount has an invalid format, ' '")
// Given, an existing project,
// When, a default destination service account with service account having backwards slash char is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"test\\sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "defaultServiceAccount has an invalid format, 'test\\\\sa'")
// Given, an existing project,
// When, a default destination service account with service account having forward slash char is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"test/sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "defaultServiceAccount has an invalid format, 'test/sa'")
// Given, an existing project,
// When, a default destination service account with service account having square braces char is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"[test-sa]",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "defaultServiceAccount has an invalid format, '[test-sa]'")
// Given, an existing project,
// When, a default destination service account with service account having curly braces char is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"test-ns",
"{test-sa}",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "defaultServiceAccount has an invalid format, '{test-sa}'")
// Given, an existing project,
// When, a default destination service account with service account having curly braces char is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"[[ech*",
"test-ns",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "server has an invalid format, '[[ech*'")
// Given, an existing project,
// When, a default destination service account with service account having curly braces char is added,
// Then, there is an error with appropriate message.
_, err = fixture.RunCli("proj", "add-destination-service-account", projectName,
"https://192.168.99.100:8443",
"[[ech*",
"test-sa",
)
require.Error(t, err)
assert.Contains(t, err.Error(), "namespace has an invalid format, '[[ech*'")
proj, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Get(context.Background(), projectName, metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, projectName, proj.Name)
assert.Len(t, proj.Spec.DestinationServiceAccounts, 3)
assert.Equal(t, "https://192.168.99.100:8443", proj.Spec.DestinationServiceAccounts[0].Server)
assert.Equal(t, "test-ns", proj.Spec.DestinationServiceAccounts[0].Namespace)
assert.Equal(t, "test-sa", proj.Spec.DestinationServiceAccounts[0].DefaultServiceAccount)
assert.Equal(t, "https://192.168.99.100:8443", proj.Spec.DestinationServiceAccounts[1].Server)
assert.Equal(t, "", proj.Spec.DestinationServiceAccounts[1].Namespace)
assert.Equal(t, "test-sa", proj.Spec.DestinationServiceAccounts[1].DefaultServiceAccount)
assert.Equal(t, "https://192.168.99.100:8443", proj.Spec.DestinationServiceAccounts[2].Server)
assert.Equal(t, "test-ns1", proj.Spec.DestinationServiceAccounts[2].Namespace)
assert.Equal(t, "default:test-sa", proj.Spec.DestinationServiceAccounts[2].DefaultServiceAccount)
assertProjHasEvent(t, proj, "update", argo.EventReasonResourceUpdated)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
@@ -16,7 +17,12 @@ import (
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/app"
)
func TestSyncWithImpersonateDisable(t *testing.T) {
const (
WaitDuration = time.Second
TimeoutDuration = time.Second * 3
)
func TestSyncWithFeatureDisabled(t *testing.T) {
Given(t).
Path("guestbook").
When().
@@ -25,10 +31,13 @@ func TestSyncWithImpersonateDisable(t *testing.T) {
app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
}).
Then().
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeSynced))
// With the impersonation feature disabled, Application sync should continue to use
// the control plane service account for the sync operation and the sync should succeed.
ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeSynced), WaitDuration, TimeoutDuration).
Expect(OperationMessageContains("successfully synced"))
}
func TestSyncWithImpersonateDefaultNamespaceServiceAccountNoRBAC(t *testing.T) {
func TestSyncWithNoDestinationServiceAccountsInProject(t *testing.T) {
Given(t).
Path("guestbook").
When().
@@ -37,37 +46,10 @@ func TestSyncWithImpersonateDefaultNamespaceServiceAccountNoRBAC(t *testing.T) {
app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
}).
Then().
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync))
}
func TestSyncWithImpersonateDefaultNamespaceServiceAccountWithRBAC(t *testing.T) {
roleName := "default-sa-role"
Given(t).
Path("guestbook").
When().
SetParamInSettingConfigMap("application.sync.impersonation.enabled", "true").
CreateFromFile(func(app *v1alpha1.Application) {
app.Spec.SyncPolicy = &v1alpha1.SyncPolicy{Automated: &v1alpha1.SyncPolicyAutomated{}}
}).
And(func() {
err := createTestRole(roleName, fixture.DeploymentNamespace(), []rbac.PolicyRule{
{
APIGroups: []string{"apps", ""},
Resources: []string{"deployments"},
Verbs: []string{"*"},
},
{
APIGroups: []string{""},
Resources: []string{"services"},
Verbs: []string{"*"},
},
})
require.NoError(t, err)
err = createTestRoleBinding(roleName, "default", fixture.DeploymentNamespace())
require.NoError(t, err)
}).
Then().
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync))
// With the impersonation feature enabled, Application sync must fail
// when there are no destination service accounts configured in AppProject
ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync), WaitDuration, TimeoutDuration).
Expect(OperationMessageContains("failed to find a matching service account to impersonate"))
}
func TestSyncWithImpersonateWithSyncServiceAccount(t *testing.T) {
@@ -89,7 +71,7 @@ func TestSyncWithImpersonateWithSyncServiceAccount(t *testing.T) {
{
Server: "*",
Namespace: fixture.DeploymentNamespace(),
DefaultServiceAccount: "false-serviceAccount",
DefaultServiceAccount: "missing-serviceAccount",
},
}
err := createTestServiceAccount(serviceAccountName, fixture.DeploymentNamespace())
@@ -118,10 +100,13 @@ func TestSyncWithImpersonateWithSyncServiceAccount(t *testing.T) {
app.Spec.Project = projectName
}).
Then().
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeSynced))
// With the impersonation feature enabled, Application sync should succeed
// as there is a valid match found in the available destination service accounts configured in AppProject
ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeSynced), WaitDuration, TimeoutDuration).
Expect(OperationMessageContains("successfully synced"))
}
func TestSyncWithImpersonateWithFalseServiceAccount(t *testing.T) {
func TestSyncWithMissingServiceAccount(t *testing.T) {
projectName := "false-test-project"
serviceAccountName := "test-account"
roleName := "test-account-sa-role"
@@ -135,7 +120,7 @@ func TestSyncWithImpersonateWithFalseServiceAccount(t *testing.T) {
{
Server: "*",
Namespace: fixture.DeploymentNamespace(),
DefaultServiceAccount: "false-serviceAccount",
DefaultServiceAccount: "missing-serviceAccount",
},
{
Server: "*",
@@ -169,11 +154,15 @@ func TestSyncWithImpersonateWithFalseServiceAccount(t *testing.T) {
app.Spec.Project = projectName
}).
Then().
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync))
// With the impersonation feature enabled, Application sync must fail
// when there is a valid match found in the available destination service accounts configured in AppProject,
// but the matching service account is missing.
ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeOutOfSync), WaitDuration, TimeoutDuration).
Expect(OperationMessageContains("one or more objects failed to apply"))
}
func TestSyncWithNegationApplicationDestinationNamespace(t *testing.T) {
projectName := "nagation-test-project"
func TestSyncWithValidSAButDisallowedDestination(t *testing.T) {
projectName := "negation-test-project"
serviceAccountName := "test-account"
roleName := "test-account-sa-role"
Given(t).
@@ -217,6 +206,7 @@ func TestSyncWithNegationApplicationDestinationNamespace(t *testing.T) {
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeSynced)).
When().
And(func() {
// Patch destination to disallow target destination namespace
patch := []byte(fmt.Sprintf(`{"spec": {"destinations": [{"namespace": "%s"}]}}`, "!"+fixture.DeploymentNamespace()))
_, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.TestNamespace()).Patch(context.Background(), projectName, types.MergePatchType, patch, metav1.PatchOptions{})
@@ -224,7 +214,10 @@ func TestSyncWithNegationApplicationDestinationNamespace(t *testing.T) {
}).
Refresh(v1alpha1.RefreshTypeNormal).
Then().
Expect(SyncStatusIs(v1alpha1.SyncStatusCodeUnknown))
// With the impersonation feature enabled, Application sync must fail
// as there is a valid match found in the available destination service accounts configured in AppProject
// but the destination namespace is now disallowed.
ExpectConsistently(SyncStatusIs(v1alpha1.SyncStatusCodeUnknown), WaitDuration, TimeoutDuration)
}
// createTestAppProject creates a test AppProject resource.

View File

@@ -4181,9 +4181,9 @@ domhandler@^4.0.0, domhandler@^4.2.0:
domelementtype "^2.2.0"
dompurify@^2.2.8:
version "2.3.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.6.tgz#2e019d7d7617aacac07cbbe3d88ae3ad354cf875"
integrity sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==
version "2.5.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.6.tgz#8402b501611eaa7fb3786072297fcbe2787f8592"
integrity sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==
domutils@^2.5.2, domutils@^2.6.0:
version "2.7.0"
@@ -6121,7 +6121,7 @@ is-wsl@^2.2.0:
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isarray@^2.0.5:
version "2.0.5"
@@ -7642,9 +7642,9 @@ path-to-regexp@0.1.10:
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
version "1.9.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24"
integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==
dependencies:
isarray "0.0.1"

View File

@@ -173,9 +173,12 @@ func cmpSupports(ctx context.Context, pluginSockFilePath, appPath, repoPath, fil
return nil, nil, false
}
// if discovery is not configured, return the client without further checks
if !cfg.IsDiscoveryConfigured {
return conn, cmpClient, true
// If discovery isn't configured but the plugin is named, then the plugin supports the repo.
if namedPlugin {
return conn, cmpClient, true
}
return nil, nil, false
}
isSupported, isDiscoveryEnabled, err := matchRepositoryCMP(ctx, appPath, repoPath, cmpClient, env, tarExcludedGlobs)

View File

@@ -631,14 +631,9 @@ func (m *nativeGitClient) lsRemote(revision string) (string, error) {
revision = "HEAD"
}
// Check if the revision is a valid semver constraint before attempting to resolve it
if constraint, err := semver.NewConstraint(revision); err == nil {
semverSha := m.resolveSemverRevision(constraint, refs)
if semverSha != "" {
return semverSha, nil
}
} else {
log.Debugf("Revision '%s' is not a valid semver constraint, skipping semver resolution.", revision)
semverSha := m.resolveSemverRevision(revision, refs)
if semverSha != "" {
return semverSha, nil
}
// refToHash keeps a maps of remote refs to their hash
@@ -684,18 +679,31 @@ func (m *nativeGitClient) lsRemote(revision string) (string, error) {
// If we get here, revision string had non hexadecimal characters (indicating its a branch, tag,
// or symbolic ref) and we were unable to resolve it to a commit SHA.
return "", fmt.Errorf("Unable to resolve '%s' to a commit SHA", revision)
return "", fmt.Errorf("unable to resolve '%s' to a commit SHA", revision)
}
// resolveSemverRevision is a part of the lsRemote method workflow.
// When the user configure correctly the Git repository revision and the revision is a valid semver constraint
// only the for loop in this function will run, otherwise the lsRemote loop will try to resolve the revision.
// Some examples to illustrate the actual behavior, if:
// * The revision is "v0.1.*"/"0.1.*" or "v0.1.2"/"0.1.2" and there's a tag matching that constraint only this function loop will run;
// * The revision is "v0.1.*"/"0.1.*" or "0.1.2"/"0.1.2" and there is no tag matching that constraint this function loop and lsRemote loop will run for backward compatibility;
// * The revision is "custom-tag" only the lsRemote loop will run because that revision is an invalid semver;
// * The revision is "master-branch" only the lsRemote loop will run because that revision is an invalid semver;
func (m *nativeGitClient) resolveSemverRevision(constraint *semver.Constraints, refs []*plumbing.Reference) string {
// When the user correctly configures the Git repository revision, and that revision is a valid semver constraint, we
// use this logic path rather than the standard lsRemote revision resolution loop.
// Some examples to illustrate the actual behavior - if the revision is:
// * "v0.1.2"/"0.1.2" or "v0.1"/"0.1", then this is not a constraint, it's a pinned version - so we fall back to the standard tag matching in the lsRemote loop.
// * "v0.1.*"/"0.1.*", and there's a tag matching that constraint, then we find the latest matching version and return its commit hash.
// * "v0.1.*"/"0.1.*", and there is *no* tag matching that constraint, then we fall back to the standard tag matching in the lsRemote loop.
// * "custom-tag", only the lsRemote loop will run - because that revision is an invalid semver;
// * "master-branch", only the lsRemote loop will run because that revision is an invalid semver;
func (m *nativeGitClient) resolveSemverRevision(revision string, refs []*plumbing.Reference) string {
if _, err := semver.NewVersion(revision); err == nil {
// If the revision is a valid version, then we know it isn't a constraint; it's just a pin.
// In which case, we should use standard tag resolution mechanisms.
return ""
}
constraint, err := semver.NewConstraint(revision)
if err != nil {
log.Debugf("Revision '%s' is not a valid semver constraint, skipping semver resolution.", revision)
return ""
}
maxVersion := semver.New(0, 0, 0, "", "")
maxVersionHash := plumbing.ZeroHash
for _, ref := range refs {
@@ -723,6 +731,7 @@ func (m *nativeGitClient) resolveSemverRevision(constraint *semver.Constraints,
return ""
}
log.Debugf("Semver constraint '%s' resolved to tag '%s', at reference '%s'", revision, maxVersion.Original(), maxVersionHash.String())
return maxVersionHash.String()
}

View File

@@ -173,6 +173,148 @@ func Test_ChangedFiles(t *testing.T) {
assert.ElementsMatch(t, []string{"README"}, changedFiles)
}
func Test_SemverTags(t *testing.T) {
tempDir := t.TempDir()
client, err := NewClientExt(fmt.Sprintf("file://%s", tempDir), tempDir, NopCreds{}, true, false, "", "")
require.NoError(t, err)
err = client.Init()
require.NoError(t, err)
mapTagRefs := map[string]string{}
for _, tag := range []string{
"v1.0.0-rc1",
"v1.0.0-rc2",
"v1.0.0",
"v1.0",
"v1.0.1",
"v1.1.0",
"2024-apple",
"2024-banana",
} {
err = runCmd(client.Root(), "git", "commit", "-m", tag+" commit", "--allow-empty")
require.NoError(t, err)
// Create an rc semver tag
err = runCmd(client.Root(), "git", "tag", tag)
require.NoError(t, err)
sha, err := client.LsRemote("HEAD")
require.NoError(t, err)
mapTagRefs[tag] = sha
}
for _, tc := range []struct {
name string
ref string
expected string
error bool
}{{
name: "pinned rc version",
ref: "v1.0.0-rc1",
expected: mapTagRefs["v1.0.0-rc1"],
}, {
name: "lt rc constraint",
ref: "< v1.0.0-rc3",
expected: mapTagRefs["v1.0.0-rc2"],
}, {
name: "pinned major version",
ref: "v1.0.0",
expected: mapTagRefs["v1.0.0"],
}, {
name: "pinned patch version",
ref: "v1.0.1",
expected: mapTagRefs["v1.0.1"],
}, {
name: "pinned minor version",
ref: "v1.1.0",
expected: mapTagRefs["v1.1.0"],
}, {
name: "patch wildcard constraint",
ref: "v1.0.*",
expected: mapTagRefs["v1.0.1"],
}, {
name: "patch tilde constraint",
ref: "~v1.0.0",
expected: mapTagRefs["v1.0.1"],
}, {
name: "minor wildcard constraint",
ref: "v1.*",
expected: mapTagRefs["v1.1.0"],
}, {
// The semver library allows for using both * and x as the wildcard modifier.
name: "alternative minor wildcard constraint",
ref: "v1.x",
expected: mapTagRefs["v1.1.0"],
}, {
name: "minor gte constraint",
ref: ">= v1.0.0",
expected: mapTagRefs["v1.1.0"],
}, {
name: "multiple constraints",
ref: "> v1.0.0 < v1.1.0",
expected: mapTagRefs["v1.0.1"],
}, {
// We treat non-specific semver versions as regular tags, rather than constraints.
name: "non-specific version",
ref: "v1.0",
expected: mapTagRefs["v1.0"],
}, {
// Which means a missing tag will raise an error.
name: "missing non-specific version",
ref: "v1.1",
error: true,
}, {
// This is NOT a semver constraint, so it should always resolve to itself - because specifying a tag should
// return the commit for that tag.
// semver/v3 has the unfortunate semver-ish behaviour where any tag starting with a number is considered to be
// "semver-ish", where that number is the semver major version, and the rest then gets coerced into a beta
// version string. This can cause unexpected behaviour with constraints logic.
// In this case, if the tag is being incorrectly coerced into semver (for being semver-ish), it will incorrectly
// return the commit for the 2024-banana tag; which we want to avoid.
name: "apple non-semver tag",
ref: "2024-apple",
expected: mapTagRefs["2024-apple"],
}, {
name: "banana non-semver tag",
ref: "2024-banana",
expected: mapTagRefs["2024-banana"],
}, {
// A semver version (without constraints) should ONLY match itself.
// We do not want "2024-apple" to get "semver-ish'ed" into matching "2024.0.0-apple"; they're different tags.
name: "no semver tag coercion",
ref: "2024.0.0-apple",
error: true,
}, {
// No minor versions are specified, so we would expect a major version of 2025 or more.
// This is because if we specify > 11 in semver, we would not expect 11.1.0 to pass; it should be 12.0.0 or more.
// Similarly, if we were to specify > 11.0, we would expect 11.1.0 or more.
name: "semver constraints on non-semver tags",
ref: "> 2024-apple",
error: true,
}, {
// However, if one specifies the minor/patch versions, semver constraints can be used to match non-semver tags.
// 2024-banana is considered as "2024.0.0-banana" in semver-ish, and banana > apple, so it's a match.
// Note: this is more for documentation and future reference than real testing, as it seems like quite odd behaviour.
name: "semver constraints on non-semver tags",
ref: "> 2024.0.0-apple",
expected: mapTagRefs["2024-banana"],
}} {
t.Run(tc.name, func(t *testing.T) {
commitSHA, err := client.LsRemote(tc.ref)
if tc.error {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.True(t, IsCommitSHA(commitSHA))
assert.Equal(t, tc.expected, commitSHA)
})
}
}
func Test_nativeGitClient_Submodule(t *testing.T) {
tempDir, err := os.MkdirTemp("", "")
require.NoError(t, err)

View File

@@ -233,15 +233,10 @@ func TestLsRemote(t *testing.T) {
expectedCommit: "ff87d8cb9e669d3738434733ecba3c6dd2c64d70",
},
{
name: "should resolve a pined tag with semantic versioning",
name: "should resolve a pinned tag with semantic versioning",
revision: "v0.8.0",
expectedCommit: "d7c04ae24c16f8ec611b0331596fbc595537abe9",
},
{
name: "should resolve a pined tag with semantic versioning without the 'v' prefix",
revision: "0.8.0",
expectedCommit: "d7c04ae24c16f8ec611b0331596fbc595537abe9",
},
{
name: "should resolve a range tag with semantic versioning",
revision: "v0.8.*", // it should resolve to v0.8.2
@@ -299,7 +294,7 @@ func TestLsRemote(t *testing.T) {
for _, revision := range xfail {
_, err := clnt.LsRemote(revision)
assert.ErrorContains(t, err, "Unable to resolve")
assert.ErrorContains(t, err, "unable to resolve")
}
})
}

View File

@@ -5,6 +5,7 @@ import (
log "github.com/sirupsen/logrus"
)
// Match tries to match a text with a given glob pattern.
func Match(pattern, text string, separators ...rune) bool {
compiledGlob, err := glob.Compile(pattern, separators...)
if err != nil {
@@ -13,3 +14,13 @@ func Match(pattern, text string, separators ...rune) bool {
}
return compiledGlob.Match(text)
}
// MatchWithError tries to match a text with a given glob pattern.
// returns error if the glob pattern fails to compile.
func MatchWithError(pattern, text string, separators ...rune) (bool, error) {
compiledGlob, err := glob.Compile(pattern, separators...)
if err != nil {
return false, err
}
return compiledGlob.Match(text), nil
}

View File

@@ -3,7 +3,7 @@ package glob
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Match(t *testing.T) {
@@ -24,7 +24,7 @@ func Test_Match(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := Match(tt.pattern, tt.input)
assert.Equal(t, tt.result, res)
require.Equal(t, tt.result, res)
})
}
}
@@ -53,7 +53,36 @@ func Test_MatchList(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := MatchStringInList(tt.list, tt.input, tt.patternMatch)
assert.Equal(t, tt.result, res)
require.Equal(t, tt.result, res)
})
}
}
func Test_MatchWithError(t *testing.T) {
tests := []struct {
name string
input string
pattern string
result bool
expectedErr string
}{
{"Exact match", "hello", "hello", true, ""},
{"Non-match exact", "hello", "hell", false, ""},
{"Long glob match", "hello", "hell*", true, ""},
{"Short glob match", "hello", "h*", true, ""},
{"Glob non-match", "hello", "e*", false, ""},
{"Invalid pattern", "e[[a*", "e[[a*", false, "unexpected end of input"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := MatchWithError(tt.pattern, tt.input)
require.Equal(t, tt.result, res)
if tt.expectedErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tt.expectedErr)
}
})
}
}

View File

@@ -141,13 +141,11 @@ func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, bool, erro
return script.HealthLua, script.UseOpenLibs, nil
}
// if not found as is, perhaps it matches wildcard entries in the configmap
wildcardKey := GetWildcardConfigMapKey(vm, obj.GroupVersionKind())
// if not found as is, perhaps it matches a wildcard entry in the configmap
getWildcardHealthOverride, useOpenLibs := getWildcardHealthOverrideLua(vm.ResourceOverrides, obj.GroupVersionKind())
if wildcardKey != "" {
if wildcardScript, ok := vm.ResourceOverrides[wildcardKey]; ok && wildcardScript.HealthLua != "" {
return wildcardScript.HealthLua, wildcardScript.UseOpenLibs, nil
}
if getWildcardHealthOverride != "" {
return getWildcardHealthOverride, useOpenLibs, nil
}
// if not found in the ResourceOverrides at all, search it as is in the built-in scripts
@@ -426,15 +424,18 @@ func GetConfigMapKey(gvk schema.GroupVersionKind) string {
return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
}
func GetWildcardConfigMapKey(vm VM, gvk schema.GroupVersionKind) string {
// getWildcardHealthOverrideLua returns the first encountered resource override which matches the wildcard and has a
// non-empty health script. Having multiple wildcards with non-empty health checks that can match the GVK is
// non-deterministic.
func getWildcardHealthOverrideLua(overrides map[string]appv1.ResourceOverride, gvk schema.GroupVersionKind) (string, bool) {
gvkKeyToMatch := GetConfigMapKey(gvk)
for key := range vm.ResourceOverrides {
if glob.Match(key, gvkKeyToMatch) {
return key
for key, override := range overrides {
if glob.Match(key, gvkKeyToMatch) && override.HealthLua != "" {
return override.HealthLua, override.UseOpenLibs
}
}
return ""
return "", false
}
func (vm VM) getPredefinedLuaScripts(objKey string, scriptFile string) (string, error) {

View File

@@ -787,6 +787,11 @@ return hs`
const healthWildcardOverrideScript = `
hs = {}
hs.status = "Healthy"
return hs`
const healthWildcardOverrideScriptUnhealthy = `
hs = {}
hs.status = "UnHealthy"
return hs`
getHealthOverride := func(openLibs bool) ResourceHealthOverrides {
@@ -804,6 +809,21 @@ return hs`
},
}
getMultipleWildcardHealthOverrides := ResourceHealthOverrides{
"*.aws.crossplane.io/*": appv1.ResourceOverride{
HealthLua: "",
},
"*.aws*": appv1.ResourceOverride{
HealthLua: healthWildcardOverrideScriptUnhealthy,
},
}
getBaseWildcardHealthOverrides := ResourceHealthOverrides{
"*/*": appv1.ResourceOverride{
HealthLua: "",
},
}
t.Run("Enable Lua standard lib", func(t *testing.T) {
testObj := StrToUnstructured(testSA)
overrides := getHealthOverride(true)
@@ -837,6 +857,23 @@ return hs`
assert.Equal(t, expectedStatus, status)
})
t.Run("Get resource health for wildcard override with non-empty health.lua", func(t *testing.T) {
testObj := StrToUnstructured(ec2AWSCrossplaneObjJson)
overrides := getMultipleWildcardHealthOverrides
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
expectedStatus := &health.HealthStatus{Status: "Unknown", Message: "Lua returned an invalid health status"}
assert.Equal(t, expectedStatus, status)
})
t.Run("Get resource health for */* override with empty health.lua", func(t *testing.T) {
testObj := StrToUnstructured(ec2AWSCrossplaneObjJson)
overrides := getBaseWildcardHealthOverrides
status, err := overrides.GetResourceHealth(testObj)
require.NoError(t, err)
assert.Nil(t, status)
})
t.Run("Resource health for wildcard override not found", func(t *testing.T) {
testObj := StrToUnstructured(testSA)
overrides := getWildcardHealthOverride

View File

@@ -535,6 +535,9 @@ const (
const (
// default max webhook payload size is 1GB
defaultMaxWebhookPayloadSize = int64(1) * 1024 * 1024 * 1024
// application sync with impersonation feature is disabled by default.
defaultImpersonationEnabledFlag = false
)
var sourceTypeToEnableGenerationKey = map[v1alpha1.ApplicationSourceType]string{
@@ -2336,11 +2339,11 @@ func (mgr *SettingsManager) GetMaxWebhookPayloadSize() int64 {
return maxPayloadSizeMB * 1024 * 1024
}
// GetIsImpersonationEnabled returns true if application sync with impersonation feature is enabled in argocd-cm configmap
func (mgr *SettingsManager) IsImpersonationEnabled() bool {
// IsImpersonationEnabled returns true if application sync with impersonation feature is enabled in argocd-cm configmap
func (mgr *SettingsManager) IsImpersonationEnabled() (bool, error) {
cm, err := mgr.getConfigMap()
if err != nil {
return false
return defaultImpersonationEnabledFlag, fmt.Errorf("error checking %s property in configmap: %w", impersonationEnabledKey, err)
}
return cm.Data[impersonationEnabledKey] == "true"
return cm.Data[impersonationEnabledKey] == "true", nil
}

View File

@@ -1725,3 +1725,46 @@ func TestRedirectAdditionalURLs(t *testing.T) {
})
}
}
func TestIsImpersonationEnabled(t *testing.T) {
// When there is no argocd-cm itself,
// Then IsImpersonationEnabled() must return false (default value) and an error with appropriate error message.
kubeClient := fake.NewSimpleClientset()
settingsManager := NewSettingsManager(context.Background(), kubeClient, "default")
featureFlag, err := settingsManager.IsImpersonationEnabled()
require.False(t, featureFlag,
"with no argocd-cm config map, IsImpersonationEnabled() must return return false (default value)")
require.ErrorContains(t, err, "configmap \"argocd-cm\" not found",
"with no argocd-cm config map, IsImpersonationEnabled() must return an error")
// When there is no impersonation feature flag present in the argocd-cm,
// Then IsImpersonationEnabled() must return false (default value) and nil error.
_, settingsManager = fixtures(map[string]string{})
featureFlag, err = settingsManager.IsImpersonationEnabled()
require.False(t, featureFlag,
"with empty argocd-cm config map, IsImpersonationEnabled() must return false (default value)")
require.NoError(t, err,
"with empty argocd-cm config map, IsImpersonationEnabled() must not return any error")
// When user disables the feature explicitly,
// Then IsImpersonationEnabled() must return false and nil error.
_, settingsManager = fixtures(map[string]string{
"application.sync.impersonation.enabled": "false",
})
featureFlag, err = settingsManager.IsImpersonationEnabled()
require.False(t, featureFlag,
"when user enables the flag in argocd-cm config map, IsImpersonationEnabled() must return user set value")
require.NoError(t, err,
"when user enables the flag in argocd-cm config map, IsImpersonationEnabled() must not return any error")
// When user enables the feature explicitly,
// Then IsImpersonationEnabled() must return true and nil error.
_, settingsManager = fixtures(map[string]string{
"application.sync.impersonation.enabled": "true",
})
featureFlag, err = settingsManager.IsImpersonationEnabled()
require.True(t, featureFlag,
"when user enables the flag in argocd-cm config map, IsImpersonationEnabled() must return user set value")
require.NoError(t, err,
"when user enables the flag in argocd-cm config map, IsImpersonationEnabled() must not return any error")
}