diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 58de56108dcd6..c34c819a22922 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -407,7 +407,7 @@ jobs: run: | docker pull quay.io/dexidp/dex:v2.25.0 docker pull argoproj/argo-cd-ci-builder:v1.0.0 - docker pull redis:7.0.0-alpine + docker pull redis:7.0.4-alpine - name: Create target directory for binaries in the build-process run: | mkdir -p dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a83fc3f4e41..0453ab6bc0b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,13 @@ commands, and helps to troubleshoot the application state. Argo CD is used to manage the critical infrastructure of multiple organizations, which makes security the top priority of the project. We've listened to your feedback and introduced additional access control settings that control access to Kubernetes Pod logs and the new Web Terminal feature. -#### Known UI Issue for Pod Logs Access +#### Pod Logs UI -Currently, upon pressing the "LOGS" tab in pod view by users who don't have an explicit allow get logs policy, the red "unable to load data: Internal error" is received in the bottom of the screen, and "Failed to load data, please try again" is displayed. +Since 2.4.9, the LOGS tab in pod view is visible in the UI only for users with explicit allow get logs policy. + +#### Known pod logs UI issue prior to 2.4.9 + +Upon pressing the "LOGS" tab in pod view by users who don't have an explicit allow get logs policy, the red "unable to load data: Internal error" is received in the bottom of the screen, and "Failed to load data, please try again" is displayed. ### OpenTelemetry Tracing Integration diff --git a/CHANGES.md b/CHANGES.md index e3041f9cf81db..0b2b4e76f42f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,11 @@ # ArgoCD Forked from: [argoproj/argo-cd](https://github.com/argoproj/argo-cd) -## v2.4.4 (Base) +## v2.4.11 (Base) Argo CD's latest stable release, as of July 7, 2022, is v2.4.4. The list of Argo CD releases can be accessed [here](https://github.com/argoproj/argo-cd/releases) -## v2.4.4 (Fork) -The changes were rebased based on v2.4.4. The following section details the enhancements made to Argo CD Extensions that were integrated in Argo CD. +## v2.4.11-patched (Fork) +The changes were rebased based on v2.4.11. The following section details the enhancements made to Argo CD Extensions that were integrated into Argo CD. ### Resource Customization ConfigMap Pulls in resource overrides from the resource customization `ConfigMap`. This `ConfigMap` will only exist if created by diff --git a/Dockerfile b/Dockerfile index 89c408090d92f..a44b59fc775af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -116,12 +116,13 @@ COPY ["ui/", "."] ARG ARGO_VERSION=latest ENV ARGO_VERSION=$ARGO_VERSION -RUN HOST_ARCH='amd64' NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OPTIONS=--max_old_space_size=8192 yarn build +ARG TARGETARCH +RUN HOST_ARCH=$TARGETARCH NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OPTIONS=--max_old_space_size=8192 yarn build #################################################################################################### # Argo CD Build stage which performs the actual build of Argo CD binaries #################################################################################################### -FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.18 AS argocd-build +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.18 AS argocd-build WORKDIR /go/src/github.com/argoproj/argo-cd diff --git a/VERSION b/VERSION index 79a614418f747..11e32126922c5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.4.4 +2.4.11 diff --git a/applicationset/services/repo_service.go b/applicationset/services/repo_service.go index 4856f269aebbe..dc8935431f4e4 100644 --- a/applicationset/services/repo_service.go +++ b/applicationset/services/repo_service.go @@ -19,8 +19,9 @@ type RepositoryDB interface { } type argoCDService struct { - repositoriesDB RepositoryDB - storecreds git.CredsStore + repositoriesDB RepositoryDB + storecreds git.CredsStore + submoduleEnabled bool } type Repos interface { @@ -32,11 +33,12 @@ type Repos interface { GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) } -func NewArgoCDService(db db.ArgoDB, gitCredStore git.CredsStore, repoServerAddress string) Repos { +func NewArgoCDService(db db.ArgoDB, gitCredStore git.CredsStore, submoduleEnabled bool) Repos { return &argoCDService{ - repositoriesDB: db.(RepositoryDB), - storecreds: gitCredStore, + repositoriesDB: db.(RepositoryDB), + storecreds: gitCredStore, + submoduleEnabled: submoduleEnabled, } } @@ -52,7 +54,7 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision s return nil, err } - err = checkoutRepo(gitRepoClient, revision) + err = checkoutRepo(gitRepoClient, revision, a.submoduleEnabled) if err != nil { return nil, err } @@ -86,7 +88,7 @@ func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revi return nil, err } - err = checkoutRepo(gitRepoClient, revision) + err = checkoutRepo(gitRepoClient, revision, a.submoduleEnabled) if err != nil { return nil, err } @@ -128,7 +130,7 @@ func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revi } -func checkoutRepo(gitRepoClient git.Client, revision string) error { +func checkoutRepo(gitRepoClient git.Client, revision string, submoduleEnabled bool) error { err := gitRepoClient.Init() if err != nil { return fmt.Errorf("Error during initializing repo: %w", err) @@ -143,7 +145,7 @@ func checkoutRepo(gitRepoClient git.Client, revision string) error { if err != nil { return fmt.Errorf("Error during fetching commitSHA: %w", err) } - err = gitRepoClient.Checkout(commitSHA, true) + err = gitRepoClient.Checkout(commitSHA, submoduleEnabled) if err != nil { return fmt.Errorf("Error during repo checkout: %w", err) } diff --git a/cmd/argocd-applicationset-controller/commands/applicationset_controller.go b/cmd/argocd-applicationset-controller/commands/applicationset_controller.go index 99ba630403395..2d7c7e192219f 100644 --- a/cmd/argocd-applicationset-controller/commands/applicationset_controller.go +++ b/cmd/argocd-applicationset-controller/commands/applicationset_controller.go @@ -18,6 +18,7 @@ import ( "github.com/argoproj/argo-cd/v2/applicationset/utils" "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/reposerver/askpass" + "github.com/argoproj/argo-cd/v2/util/env" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -37,6 +38,11 @@ import ( argosettings "github.com/argoproj/argo-cd/v2/util/settings" ) +// TODO: load this using Cobra. https://github.com/argoproj/argo-cd/issues/10157 +func getSubmoduleEnabled() bool { + return env.ParseBoolFromEnv(common.EnvGitSubmoduleEnabled, true) +} + func NewCommand() *cobra.Command { var ( clientConfig clientcmd.ClientConfig @@ -136,7 +142,7 @@ func NewCommand() *cobra.Command { terminalGenerators := map[string]generators.Generator{ "List": generators.NewListGenerator(), "Clusters": generators.NewClusterGenerator(mgr.GetClient(), context.Background(), k8sClient, namespace), - "Git": generators.NewGitGenerator(services.NewArgoCDService(argoCDDB, askPassServer, argocdRepoServer)), + "Git": generators.NewGitGenerator(services.NewArgoCDService(argoCDDB, askPassServer, getSubmoduleEnabled())), "SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient()), "ClusterDecisionResource": generators.NewDuckTypeGenerator(context.Background(), dynamicClient, k8sClient, namespace), "PullRequest": generators.NewPullRequestGenerator(mgr.GetClient()), diff --git a/cmd/argocd/commands/login.go b/cmd/argocd/commands/login.go index d58b8ebc0a938..7f647a56f8eac 100644 --- a/cmd/argocd/commands/login.go +++ b/cmd/argocd/commands/login.go @@ -68,7 +68,8 @@ argocd login cd.argoproj.io --core`, server = "kubernetes" } else { server = args[0] - tlsTestResult, err := grpc_util.TestTLS(server) + dialTime := 30 * time.Second + tlsTestResult, err := grpc_util.TestTLS(server, dialTime) errors.CheckError(err) if !tlsTestResult.TLS { if !globalClientOpts.PlainText { diff --git a/cmpserver/server.go b/cmpserver/server.go index 636dbcd1f7339..f66da10605695 100644 --- a/cmpserver/server.go +++ b/cmpserver/server.go @@ -2,12 +2,13 @@ package cmpserver import ( "fmt" - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "net" "os" "os/signal" "syscall" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" @@ -24,6 +25,7 @@ import ( "github.com/argoproj/argo-cd/v2/server/version" "github.com/argoproj/argo-cd/v2/util/errors" grpc_util "github.com/argoproj/argo-cd/v2/util/grpc" + "google.golang.org/grpc/keepalive" ) // ArgoCDCMPServer is the config management plugin server implementation @@ -61,6 +63,11 @@ func NewServer(initConstants plugin.CMPServerInitConstants) (*ArgoCDCMPServer, e grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(streamInterceptors...)), grpc.MaxRecvMsgSize(apiclient.MaxGRPCMessageSize), grpc.MaxSendMsgSize(apiclient.MaxGRPCMessageSize), + grpc.KeepaliveEnforcementPolicy( + keepalive.EnforcementPolicy{ + MinTime: common.GRPCKeepAliveEnforcementMinimum, + }, + ), } return &ArgoCDCMPServer{ diff --git a/common/common.go b/common/common.go index d63dca59412f1..68d1785a62b88 100644 --- a/common/common.go +++ b/common/common.go @@ -300,3 +300,10 @@ const ( // AnnotationApplicationRefresh is an annotation that is added when an ApplicationSet is requested to be refreshed by a webhook. The ApplicationSet controller will remove this annotation at the end of reconcilation. AnnotationApplicationSetRefresh = "argocd.argoproj.io/application-set-refresh" ) + +// gRPC settings +const ( + GRPCKeepAliveEnforcementMinimum = 10 * time.Second + // Keep alive is 2x enforcement minimum to ensure network jitter does not introduce ENHANCE_YOUR_CALM errors + GRPCKeepAliveTime = 2 * GRPCKeepAliveEnforcementMinimum +) diff --git a/controller/cache/cache.go b/controller/cache/cache.go index 4149cc09c1a62..c288aceb2310d 100644 --- a/controller/cache/cache.go +++ b/controller/cache/cache.go @@ -176,6 +176,7 @@ func NewLiveStateCache( type cacheSettings struct { clusterSettings clustercache.Settings appInstanceLabelKey string + trackingMethod appv1.TrackingMethod } type liveStateCache struct { @@ -210,7 +211,7 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) { ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides), ResourcesFilter: resourcesFilter, } - return &cacheSettings{clusterSettings, appInstanceLabelKey}, nil + return &cacheSettings{clusterSettings, appInstanceLabelKey, argo.GetTrackingMethod(c.settingsMgr)}, nil } func asResourceNode(r *clustercache.Resource) appv1.ResourceNode { @@ -354,7 +355,8 @@ func isTransientNetworkErr(err error) bool { } if strings.Contains(errorString, "net/http: TLS handshake timeout") || strings.Contains(errorString, "i/o timeout") || - strings.Contains(errorString, "connection timed out") { + strings.Contains(errorString, "connection timed out") || + strings.Contains(errorString, "connection reset by peer") { return true } return false @@ -387,7 +389,6 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e return nil, fmt.Errorf("controller is configured to ignore cluster %s", cluster.Server) } - trackingMethod := argo.GetTrackingMethod(c.settingsMgr) clusterCacheOpts := []clustercache.UpdateSettingsFunc{ clustercache.SetListSemaphore(semaphore.NewWeighted(clusterCacheListSemaphoreSize)), clustercache.SetListPageSize(clusterCacheListPageSize), @@ -400,9 +401,12 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e clustercache.SetPopulateResourceInfoHandler(func(un *unstructured.Unstructured, isRoot bool) (interface{}, bool) { res := &ResourceInfo{} populateNodeInfo(un, res) + c.lock.RLock() + cacheSettings := c.cacheSettings + c.lock.RUnlock() res.Health, _ = health.GetResourceHealth(un, cacheSettings.clusterSettings.ResourceHealthOverride) - appName := c.resourceTracking.GetAppName(un, cacheSettings.appInstanceLabelKey, trackingMethod) + appName := c.resourceTracking.GetAppName(un, cacheSettings.appInstanceLabelKey, cacheSettings.trackingMethod) if isRoot && appName != "" { res.AppName = appName } diff --git a/controller/cache/cache_test.go b/controller/cache/cache_test.go index aa6f26540b618..dcff97edb2157 100644 --- a/controller/cache/cache_test.go +++ b/controller/cache/cache_test.go @@ -111,6 +111,7 @@ func TestIsRetryableError(t *testing.T) { tlsHandshakeTimeoutErr net.Error = netError("net/http: TLS handshake timeout") ioTimeoutErr net.Error = netError("i/o timeout") connectionTimedout net.Error = netError("connection timed out") + connectionReset net.Error = netError("connection reset by peer") ) t.Run("Nil", func(t *testing.T) { assert.False(t, isRetryableError(nil)) @@ -148,4 +149,7 @@ func TestIsRetryableError(t *testing.T) { t.Run("ConnectionTimeout", func(t *testing.T) { assert.True(t, isRetryableError(connectionTimedout)) }) + t.Run("ConnectionReset", func(t *testing.T) { + assert.True(t, isRetryableError(connectionReset)) + }) } diff --git a/controller/state.go b/controller/state.go index 9b9a3ab225cd8..095a80e550a2b 100644 --- a/controller/state.go +++ b/controller/state.go @@ -494,6 +494,8 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap } gvk := obj.GroupVersionKind() + isSelfReferencedObj := m.isSelfReferencedObj(liveObj, appLabelKey, trackingMethod) + resState := v1alpha1.ResourceStatus{ Namespace: obj.GetNamespace(), Name: obj.GetName(), @@ -501,7 +503,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap Version: gvk.Version, Group: gvk.Group, Hook: hookutil.IsHook(obj), - RequiresPruning: targetObj == nil && liveObj != nil, + RequiresPruning: targetObj == nil && liveObj != nil && isSelfReferencedObj, } var diffResult diff.DiffResult @@ -510,8 +512,11 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap } else { diffResult = diff.DiffResult{Modified: false, NormalizedLive: []byte("{}"), PredictedLive: []byte("{}")} } - if resState.Hook || ignore.Ignore(obj) || (targetObj != nil && hookutil.Skip(targetObj)) { - // For resource hooks or skipped resources, don't store sync status, and do not affect overall sync status + if resState.Hook || ignore.Ignore(obj) || (targetObj != nil && hookutil.Skip(targetObj)) || !isSelfReferencedObj { + // For resource hooks, skipped resources or objects that may have + // been created by another controller with annotations copied from + // the source object, don't store sync status, and do not affect + // overall sync status } else if diffResult.Modified || targetObj == nil || liveObj == nil { // Set resource state to OutOfSync since one of the following is true: // * target and live resource are different @@ -667,3 +672,34 @@ func NewAppStateManager( resourceTracking: resourceTracking, } } + +// isSelfReferencedObj returns whether the given obj is managed by the application +// according to the values in the tracking annotation. It returns true when all +// of the properties in the annotation (name, namespace, group and kind) match +// the properties of the inspected object, or if the tracking method used does +// not provide the required properties for matching. +func (m *appStateManager) isSelfReferencedObj(obj *unstructured.Unstructured, appLabelKey string, trackingMethod v1alpha1.TrackingMethod) bool { + if obj == nil { + return true + } + + // If tracking method doesn't contain required metadata for this check, + // we are not able to determine and just assume the object to be managed. + if trackingMethod == argo.TrackingMethodLabel { + return true + } + + // In order for us to assume obj to be managed by this application, the + // values from the annotation have to match the properties from the live + // object. Cluster scoped objects carry the app's destination namespace + // in the tracking annotation, but are unique in GVK + name combination. + appInstance := m.resourceTracking.GetAppInstance(obj, appLabelKey, trackingMethod) + if appInstance != nil { + return (obj.GetNamespace() == appInstance.Namespace || obj.GetNamespace() == "") && + obj.GetName() == appInstance.Name && + obj.GetObjectKind().GroupVersionKind().Group == appInstance.Group && + obj.GetObjectKind().GroupVersionKind().Kind == appInstance.Kind + } + + return true +} diff --git a/controller/state_test.go b/controller/state_test.go index 0dba8b0c3a159..f5428e7b81da4 100644 --- a/controller/state_test.go +++ b/controller/state_test.go @@ -13,6 +13,7 @@ import ( . "github.com/argoproj/gitops-engine/pkg/utils/testing" "github.com/stretchr/testify/assert" v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -21,6 +22,7 @@ import ( argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/test" + "github.com/argoproj/argo-cd/v2/util/argo" ) // TestCompareAppStateEmpty tests comparison when both git and live have no objects @@ -770,3 +772,128 @@ func TestComparisonResult_GetSyncStatus(t *testing.T) { assert.Equal(t, status, res.GetSyncStatus()) } + +func TestIsLiveResourceManaged(t *testing.T) { + managedObj := kube.MustToUnstructured(&corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap1", + Namespace: "default", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1", + }, + }, + }) + managedObjWithLabel := kube.MustToUnstructured(&corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap1", + Namespace: "default", + Labels: map[string]string{ + common.LabelKeyAppInstance: "guestbook", + }, + }, + }) + unmanagedObjWrongName := kube.MustToUnstructured(&corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap2", + Namespace: "default", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:default/configmap1", + }, + }, + }) + unmanagedObjWrongKind := kube.MustToUnstructured(&corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap2", + Namespace: "default", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: "guestbook:/Service:default/configmap2", + }, + }, + }) + unmanagedObjWrongGroup := kube.MustToUnstructured(&corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap2", + Namespace: "default", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: "guestbook:apps/ConfigMap:default/configmap2", + }, + }, + }) + unmanagedObjWrongNamespace := kube.MustToUnstructured(&corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap2", + Namespace: "default", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: "guestbook:/ConfigMap:fakens/configmap2", + }, + }, + }) + ctrl := newFakeController(&fakeData{ + apps: []runtime.Object{app, &defaultProj}, + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(managedObj): managedObj, + kube.GetResourceKey(unmanagedObjWrongName): unmanagedObjWrongName, + kube.GetResourceKey(unmanagedObjWrongKind): unmanagedObjWrongKind, + kube.GetResourceKey(unmanagedObjWrongGroup): unmanagedObjWrongGroup, + kube.GetResourceKey(unmanagedObjWrongNamespace): unmanagedObjWrongNamespace, + }, + }) + + manager := ctrl.appStateManager.(*appStateManager) + + // Managed resource w/ annotations + assert.True(t, manager.isSelfReferencedObj(managedObj, common.AnnotationKeyAppInstance, argo.TrackingMethodLabel)) + assert.True(t, manager.isSelfReferencedObj(managedObj, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation)) + + // Managed resource w/ label + assert.True(t, manager.isSelfReferencedObj(managedObjWithLabel, common.AnnotationKeyAppInstance, argo.TrackingMethodLabel)) + + // Wrong resource name + assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongName, common.AnnotationKeyAppInstance, argo.TrackingMethodLabel)) + assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongName, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation)) + + // Wrong resource group + assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongGroup, common.AnnotationKeyAppInstance, argo.TrackingMethodLabel)) + assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongGroup, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation)) + + // Wrong resource kind + assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongKind, common.AnnotationKeyAppInstance, argo.TrackingMethodLabel)) + assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongKind, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation)) + + // Wrong resource namespace + assert.True(t, manager.isSelfReferencedObj(unmanagedObjWrongNamespace, common.AnnotationKeyAppInstance, argo.TrackingMethodLabel)) + assert.False(t, manager.isSelfReferencedObj(unmanagedObjWrongNamespace, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotationAndLabel)) + + // Nil resource + assert.True(t, manager.isSelfReferencedObj(nil, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation)) +} diff --git a/controller/sync.go b/controller/sync.go index 3678aed714e39..38b035609c163 100644 --- a/controller/sync.go +++ b/controller/sync.go @@ -205,6 +205,13 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha reconciliationResult.Target = patchedTargets } + appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey() + if err != nil { + log.Errorf("Could not get appInstanceLabelKey: %v", err) + return + } + trackingMethod := argo.GetTrackingMethod(m.settingsMgr) + syncCtx, cleanup, err := sync.NewSyncContext( compareResult.syncStatus.Revision, reconciliationResult, @@ -217,7 +224,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha sync.WithHealthOverride(lua.ResourceHealthOverrides(resourceOverrides)), sync.WithPermissionValidator(func(un *unstructured.Unstructured, res *v1.APIResource) error { if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), res.Namespaced) { - return fmt.Errorf("Resource %s:%s is not permitted in project %s.", un.GroupVersionKind().Group, un.GroupVersionKind().Kind, proj.Name) + return fmt.Errorf("resource %s:%s is not permitted in project %s", un.GroupVersionKind().Group, un.GroupVersionKind().Kind, proj.Name) } if res.Namespaced && !proj.IsDestinationPermitted(v1alpha1.ApplicationDestination{Namespace: un.GetNamespace(), Server: app.Spec.Destination.Server, Name: app.Spec.Destination.Name}) { return fmt.Errorf("namespace %v is not permitted in project '%s'", un.GetNamespace(), proj.Name) @@ -227,7 +234,9 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha sync.WithOperationSettings(syncOp.DryRun, syncOp.Prune, syncOp.SyncStrategy.Force(), syncOp.IsApplyStrategy() || len(syncOp.Resources) > 0), sync.WithInitialState(state.Phase, state.Message, initialResourcesRes, state.StartedAt), sync.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool { - return len(syncOp.Resources) == 0 || argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources) + return (len(syncOp.Resources) == 0 || + argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)) && + m.isSelfReferencedObj(live, appLabelKey, trackingMethod) }), sync.WithManifestValidation(!syncOp.SyncOptions.HasOption(common.SyncOptionsDisableValidation)), sync.WithNamespaceCreation(syncOp.SyncOptions.HasOption("CreateNamespace=true"), func(un *unstructured.Unstructured) bool { diff --git a/docs/assets/terminal.png b/docs/assets/terminal.png new file mode 100644 index 0000000000000..2e89ddc4f6726 Binary files /dev/null and b/docs/assets/terminal.png differ diff --git a/docs/developer-guide/toolchain-guide.md b/docs/developer-guide/toolchain-guide.md index 909d88fbbae7e..a80a88c875c22 100644 --- a/docs/developer-guide/toolchain-guide.md +++ b/docs/developer-guide/toolchain-guide.md @@ -24,8 +24,7 @@ You will need at least the following things in your toolchain in order to develo * A Kubernetes cluster. You won't need a fully blown multi-master, multi-node cluster, but you will need something like K3S, Minikube or microk8s. You will also need a working Kubernetes client (`kubectl`) configuration in your development environment. The configuration must reside in `~/.kube/config` and the API server URL must point to the IP address of your local machine (or VM), and **not** to `localhost` or `127.0.0.1` if you are using the virtualized development toolchain (see below) -* You will also need a working Docker runtime environment, to be able to build and run images. -The Docker version must be fairly recent, and support multi-stage builds. You should not work as root. Make your local user a member of the `docker` group to be able to control the Docker service on your machine. +* You will also need a working Docker runtime environment, to be able to build and run images. The Docker version must be 17.05.0 or higher, to support multi-stage builds. * Obviously, you will need a `git` client for pulling source code and pushing back your changes. diff --git a/docs/developer-guide/ui-extensions.md b/docs/developer-guide/ui-extensions.md new file mode 100644 index 0000000000000..b088bb72ccadf --- /dev/null +++ b/docs/developer-guide/ui-extensions.md @@ -0,0 +1,64 @@ +# UI Extensions + +Argo CD web user interface can be extended with additional UI elements. Extensions should be delivered as a javascript file +in the `argocd-server` Pods that are placed in the `/tmp/extensions` directory and starts with `extension` prefix ( matches to `^extension(.*)\.js$` regex ). + +``` +/tmp/extensions +├── example1 +│   └── extension-1.js +└── example2 + └── extension-2.js +``` + +Extensions are loaded during initial page rendering and should register themselves using API exposed in the `extensionsAPI` global variable. (See +corresponding extension type details for additional information). + +The extension should provide a React component that is responsible for rendering the UI element. Extension should not bundle the React library. +Instead extension should use the `react` global variable. You can leverage `externals` setting if you are using webpack: + +```js + externals: { + react: 'React' + } +``` + +## Resource Tab Extensions + +Resource Tab extensions is an extension that provides an additional tab for the resource sliding panel at the Argo CD Application details page. + +The resource tab extension should be registered using the `extensionsAPI.registerResourceExtension` method: + +```typescript +registerResourceExtension(component: ExtensionComponent, group: string, kind: string, tabTitle: string) +``` + +* `component: ExtensionComponent` is a React component that receives the following properties: + + * application: Application - Argo CD Application resource; + * resource: State - the kubernetes resource object; + * tree: ApplicationTree - includes list of all resources that comprise the application; + + See properties interfaces in [models.ts](https://github.com/argoproj/argo-cd/blob/master/ui/src/app/shared/models.ts) + +* `group: string` - the glob expression that matches the group of the resource; note: use globstar (`**`) to match all groups including empty string; +* `kind: string` - the glob expression that matches the kind of the resource; +* `tabTitle: string` - the extension tab title. +* `opts: Object` - additional options: + * `icon: string` - the class name the represents the icon from the [https://fontawesome.com/](https://fontawesome.com/) library (e.g. 'fa-calendar-alt'); + +Below is an example of a resource tab extension: + +```javascript +((window) => { + const component = () => { + return React.createElement( 'div', {}, 'Hello World' ); + }; + window.extensionsAPI.registerResourceExtension(component, '*', '*', 'Nice extension'); +})(window) +``` + +## Application Tab Extensions + +Since the Argo CD Application is a Kubernetes resource, application tabs can be the same as any other resource tab. +Make sure to use 'argoproj.io'/'Application' as group/kind and an extension will be used to render the application-level tab. \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index 85612570173f4..c0176354521f1 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -174,7 +174,9 @@ argocd ... --grpc-web ## Why Am I Getting `x509: certificate signed by unknown authority` When Using The CLI? -Your not running your server with correct certs. +The certificate created by default by Argo CD is not automatically recognised by the Argo CD CLI, in order +to create a secure system you must follow the instructions to [install a certificate](/operator-manual/tls/) +and configure your client OS to trust that certificate. If you're not running in a production system (e.g. you're testing Argo CD out), try the `--insecure` flag: diff --git a/docs/getting_started.md b/docs/getting_started.md index 65d611fb994dd..b327d796263ce 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -29,6 +29,13 @@ kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/core-install.yaml ``` +This default installation will have a self-signed certificate and cannot be accessed without a bit of extra work. +Do one of: + +* Follow the [instructions to configure a certificate](./operator-manual/tls) (and ensure that the client OS trusts it). +* Configure the client OS to trust the self signed certificate. +* Use the --insecure flag on all Argo CD CLI operations in this guide. + Use `argocd login --core` to [configure](./user-guide/commands/argocd_login.md) CLI access and skip steps 3-5. ## 2. Download Argo CD CLI diff --git a/docs/operator-manual/application.yaml b/docs/operator-manual/application.yaml index a61d02b8d35bf..699a8908e65b3 100644 --- a/docs/operator-manual/application.yaml +++ b/docs/operator-manual/application.yaml @@ -69,11 +69,16 @@ spec: kustomize: # Optional kustomize version. Note: version must be configured in argocd-cm ConfigMap version: v3.5.4 - # Optional image name prefix + # Supported kustomize transformers. https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/ namePrefix: prod- - # Optional images passed to "kustomize edit set image". + nameSuffix: -some-suffix + commonLabels: + foo: bar + commonAnnotations: + beep: boop images: - gcr.io/heptio-images/ks-guestbook-demo:0.2 + - my-app=gcr.io/my-repo/my-app:0.1 # directory directory: @@ -92,6 +97,12 @@ spec: - code: false name: foo value: bar + # Exclude contains a glob pattern to match paths against that should be explicitly excluded from being used during + # manifest generation. This takes precedence over the `include` field. + exclude: string + # Include contains a glob pattern to match paths against that should be explicitly included during manifest + # generation. If this field is set, only matching manifests will be included. + include: string # plugin specific config plugin: diff --git a/docs/operator-manual/applicationset/Generators-Git.md b/docs/operator-manual/applicationset/Generators-Git.md index 78de8f51b9a17..b235602e36492 100644 --- a/docs/operator-manual/applicationset/Generators-Git.md +++ b/docs/operator-manual/applicationset/Generators-Git.md @@ -107,7 +107,7 @@ spec: ``` (*The full example can be found [here](https://github.com/argoproj/argo-cd/tree/master/examples/applicationset/git-generator-directory/excludes).*) -This example excludes the `exclude-helm-guestbook` directory from the list of directories scanned for this `ApplictionSet` resource. +This example excludes the `exclude-helm-guestbook` directory from the list of directories scanned for this `ApplicationSet` resource. !!! note "Exclude rules have higher priority than include rules" diff --git a/docs/operator-manual/applicationset/Generators-Matrix.md b/docs/operator-manual/applicationset/Generators-Matrix.md index 3ed9b6469c84c..a075b757dd5eb 100644 --- a/docs/operator-manual/applicationset/Generators-Matrix.md +++ b/docs/operator-manual/applicationset/Generators-Matrix.md @@ -107,32 +107,33 @@ Finally, the Matrix generator will combine both sets of outputs, and produce: ## Restrictions 1. The Matrix generator currently only supports combining the outputs of only two child generators (eg does not support generating combinations for 3 or more). + 1. You should specify only a single generator per array entry, eg this is not valid: -```yaml -- matrix: - generators: - - list: # (...) - git: # (...) -``` + + - matrix: + generators: + - list: # (...) + git: # (...) + - While this *will* be accepted by Kubernetes API validation, the controller will report an error on generation. Each generator should be specified in a separate array element, as in the examples above. + 1. The Matrix generator does not currently support [`template` overrides](Template.md#generator-templates) specified on child generators, eg this `template` will not be processed: -```yaml -- matrix: - generators: - - list: - elements: - - # (...) - template: { } # Not processed -``` + + - matrix: + generators: + - list: + elements: + - # (...) + template: { } # Not processed + 1. Combination-type generators (matrix or merge) can only be nested once. For example, this will not work: -```yaml -- matrix: - generators: - - matrix: - generators: - - matrix: # This third level is invalid. - generators: - - list: - elements: - - # (...) -``` + + - matrix: + generators: + - matrix: + generators: + - matrix: # This third level is invalid. + generators: + - list: + elements: + - # (...) diff --git a/docs/operator-manual/applicationset/Generators-Merge.md b/docs/operator-manual/applicationset/Generators-Merge.md index fd2501d26e17a..eb32343accbf4 100644 --- a/docs/operator-manual/applicationset/Generators-Merge.md +++ b/docs/operator-manual/applicationset/Generators-Merge.md @@ -114,31 +114,31 @@ When merged with the updated base parameters, the `values.redis` value for the p ## Restrictions 1. You should specify only a single generator per array entry. This is not valid: -```yaml -- merge: - generators: - - list: # (...) - git: # (...) -``` + + - merge: + generators: + - list: # (...) + git: # (...) + - While this *will* be accepted by Kubernetes API validation, the controller will report an error on generation. Each generator should be specified in a separate array element, as in the examples above. + 1. The Merge generator does not support [`template` overrides](Template.md#generator-templates) specified on child generators. This `template` will not be processed: -```yaml -- merge: - generators: - - list: - elements: - - # (...) - template: { } # Not processed -``` + + - merge: + generators: + - list: + elements: + - # (...) + template: { } # Not processed + 1. Combination-type generators (Matrix or Merge) can only be nested once. For example, this will not work: -```yaml -- merge: - generators: - - merge: - generators: - - merge: # This third level is invalid. - generators: - - list: - elements: - - # (...) -``` + + - merge: + generators: + - merge: + generators: + - merge: # This third level is invalid. + generators: + - list: + elements: + - # (...) diff --git a/docs/operator-manual/applicationset/Generators-SCM-Provider.md b/docs/operator-manual/applicationset/Generators-SCM-Provider.md index ee430fcd0778b..ab3a5cff66a8c 100644 --- a/docs/operator-manual/applicationset/Generators-SCM-Provider.md +++ b/docs/operator-manual/applicationset/Generators-SCM-Provider.md @@ -76,7 +76,7 @@ spec: gitlab: # The base GitLab group to scan. You can either use the group id or the full namespaced path. group: "8675309" - # For GitLab Enterprise: + # For self-hosted GitLab: api: https://gitlab.example.com/ # If true, scan every branch of every repository. If false, scan only the default branch. Defaults to false. allBranches: true @@ -91,7 +91,7 @@ spec: ``` * `group`: Required name of the base GitLab group to scan. If you have multiple base groups, use multiple generators. -* `api`: If using GitHub Enterprise, the URL to access it. +* `api`: If using self-hosted GitLab, the URL to access it. * `allBranches`: By default (false) the template will only be evaluated for the default branch of each repo. If this is true, every branch of every repository will be passed to the filters. If using this flag, you likely want to use a `branchMatch` filter. * `includeSubgroups`: By default (false) the controller will only search for repos directly in the base group. If this is true, it will recurse through all the subgroups searching for repos to scan. * `tokenRef`: A `Secret` name and key containing the GitLab access token to use for requests. If not specified, will make anonymous requests which have a lower rate limit and can only see public repositories. diff --git a/docs/operator-manual/applicationset/Generators.md b/docs/operator-manual/applicationset/Generators.md index 12e6440818fe7..cd61db5da7918 100644 --- a/docs/operator-manual/applicationset/Generators.md +++ b/docs/operator-manual/applicationset/Generators.md @@ -4,7 +4,7 @@ Generators are responsible for generating *parameters*, which are then rendered Generators are primarily based on the data source that they use to generate the template parameters. For example: the List generator provides a set of parameters from a *literal list*, the Cluster generator uses the *Argo CD cluster list* as a source, the Git generator uses files/directories from a *Git repository*, and so. -As of this writing there are seven generators: +As of this writing there are eight generators: - [List generator](Generators-List.md): The List generator allows you to target Argo CD Applications to clusters based on a fixed list of cluster name/URL values. - [Cluster generator](Generators-Cluster.md): The Cluster generator allows you to target Argo CD Applications to clusters, based on the list of clusters defined within (and managed by) Argo CD (which includes automatically responding to cluster addition/removal events from Argo CD). diff --git a/docs/operator-manual/applicationset/Security.md b/docs/operator-manual/applicationset/Security.md index 6c3d656cbd427..5df7a797de300 100644 --- a/docs/operator-manual/applicationset/Security.md +++ b/docs/operator-manual/applicationset/Security.md @@ -11,8 +11,8 @@ resources of Argo CD itself (like the RBAC ConfigMap). ApplicationSets can also quickly create an arbitrary number of Applications and just as quickly delete them. Finally, ApplicationSets can reveal privileged information. For example, the [git generator](./Generators-Git.md) can -read Secrets in the Argo CD namespace and send them to arbitrary URLs as auth headers. (This functionality is intended -for authorizing requests to SCM providers like GitHub, but it could be abused by a malicious user.) +read Secrets in the Argo CD namespace and send them to arbitrary URLs (e.g. URL provided for the `api` field) as auth headers. +(This functionality is intended for authorizing requests to SCM providers like GitHub, but it could be abused by a malicious user.) For these reasons, **only admins** may be given permission (via Kubernetes RBAC or any other mechanism) to create, update, or delete ApplicationSets. diff --git a/docs/operator-manual/argocd-cm.yaml b/docs/operator-manual/argocd-cm.yaml index e831486edba85..a378a5f154ab5 100644 --- a/docs/operator-manual/argocd-cm.yaml +++ b/docs/operator-manual/argocd-cm.yaml @@ -36,9 +36,12 @@ data: help.chatUrl: "https://mycorp.slack.com/argo-cd" # the text for getting chat help, defaults to "Chat now!" help.chatText: "Chat now!" - # The URLs to download additional ArgoCD binaries (besides the Linux amd64 binary included by default) + # The URLs to download additional ArgoCD binaries (besides the Linux with current platform binary included by default) # for different OS architectures. If provided, additional download buttons will be displayed on the help page. + help.download.linux-amd64: "path-or-url-to-download" help.download.linux-arm64: "path-or-url-to-download" + help.download.linux-ppc64le: "path-or-url-to-download" + help.download.linux-s390x: "path-or-url-to-download" help.download.darwin-amd64: "path-or-url-to-download" help.download.darwin-arm64: "path-or-url-to-download" help.download.windows-amd64: "path-or-url-to-download" @@ -231,6 +234,14 @@ data: # If omitted, Argo CD injects the app name into the label: 'app.kubernetes.io/instance' application.instanceLabelKey: mycompany.com/appname + # You can change the resource tracking method Argo CD uses by changing the + # setting application.resourceTrackingMethod to the desired method. + # The following methods are available: + # - label : Uses the application.instanceLabelKey label for tracking + # - annotation : Uses an annotation with additional metadata for tracking instead of the label + # - annotation+label : Also uses an annotation for tracking, but additionally labels the resource with the application name + application.resourceTrackingMethod: annotation + # disables admin user. Admin is enabled by default admin.enabled: "false" # add an additional local user with apiKey and login capabilities @@ -288,3 +299,12 @@ data: # exec.enabled indicates whether the UI exec feature is enabled. It is disabled by default. exec.enabled: "false" + + # exec.shells restricts which shells are allowed for `exec`, and in which order they are attempted + exec.shells: "bash,sh,powershell,cmd" + + # oidc.tls.insecure.skip.verify determines whether certificate verification is skipped when verifying tokens with the + # configured OIDC provider (either external or the bundled Dex instance). Setting this to "true" will cause JWT + # token verification to pass despite the OIDC provider having an invalid certificate. Only set to "true" if you + # understand the risks. + oidc.tls.insecure.skip.verify: "false" diff --git a/docs/operator-manual/declarative-setup.md b/docs/operator-manual/declarative-setup.md index 83e517f0b396a..4f0a57c31bc7c 100644 --- a/docs/operator-manual/declarative-setup.md +++ b/docs/operator-manual/declarative-setup.md @@ -489,6 +489,7 @@ The secret data must include following fields: * `name` - cluster name * `server` - cluster api server url * `namespaces` - optional comma-separated list of namespaces which are accessible in that cluster. Cluster level resources would be ignored if namespace list is not empty. +* `clusterResources` - optional boolean string (`"true"` or `"false"`) determining whether Argo CD can manage cluster-level resources on this cluster. This setting is used only if the list of managed namespaces is not empty. * `config` - JSON representation of following data structure: ```yaml diff --git a/docs/operator-manual/high_availability.md b/docs/operator-manual/high_availability.md index 5c1416984ecc1..debc25617bc7f 100644 --- a/docs/operator-manual/high_availability.md +++ b/docs/operator-manual/high_availability.md @@ -119,7 +119,7 @@ If the manifest generation has no side effects then requests are processed in pa * **Multiple Helm based applications pointing to the same directory in one Git repository:** ensure that your Helm chart don't have conditional [dependencies](https://helm.sh/docs/chart_best_practices/dependencies/#conditions-and-tags) and create `.argocd-allow-concurrency` file in chart directory. - * **Multiple Custom plugin based applications:** avoid creating temporal files during manifest generation and create `.argocd-allow-concurrency` file in app directory. + * **Multiple Custom plugin based applications:** avoid creating temporal files during manifest generation and create `.argocd-allow-concurrency` file in app directory, or use the sidecar plugin option, which processes each application using a temporary copy of the repository. * **Multiple Kustomize applications in same repository with [parameter overrides](../user-guide/parameters.md):** sorry, no workaround for now. diff --git a/docs/operator-manual/ingress.md b/docs/operator-manual/ingress.md index 966654cff9696..e8801960f1f35 100644 --- a/docs/operator-manual/ingress.md +++ b/docs/operator-manual/ingress.md @@ -12,7 +12,7 @@ There are several ways how Ingress can be configured. The Ambassador Edge Stack can be used as a Kubernetes ingress controller with [automatic TLS termination](https://www.getambassador.io/docs/latest/topics/running/tls/#host) and routing capabilities for both the CLI and the UI. -The API server should be run with TLS disabled. Edit the `argocd-server` deployment to add the `--insecure` flag to the argocd-server command. Given the `argocd` CLI includes the port number in the request `host` header, 2 Mappings are required. +The API server should be run with TLS disabled. Edit the `argocd-server` deployment to add the `--insecure` flag to the argocd-server command, or simply set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). Given the `argocd` CLI includes the port number in the request `host` header, 2 Mappings are required. ### Option 1: Mapping CRD for Host-based Routing ```yaml @@ -72,7 +72,7 @@ argocd login : --grpc-web-root-path /argo-cd ## [Contour](https://projectcontour.io/) The Contour ingress controller can terminate TLS ingress traffic at the edge. -The Argo CD API server should be run with TLS disabled. Edit the `argocd-server` Deployment to add the `--insecure` flag to the argocd-server container command. +The Argo CD API server should be run with TLS disabled. Edit the `argocd-server` Deployment to add the `--insecure` flag to the argocd-server container command, or simply set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). It is also possible to provide an internal-only ingress path and an external-only ingress path by deploying two instances of Contour: one behind a private-subnet LoadBalancer service and one behind a public-subnet LoadBalancer service. The private Contour deployment will pick up Ingresses annotated with `kubernetes.io/ingress.class: contour-internal` and the public Contour deployment will pick up Ingresses annotated with `kubernetes.io/ingress.class: contour-external`. @@ -164,20 +164,7 @@ spec: The argocd-server Service needs to be annotated with `projectcontour.io/upstream-protocol.h2c: "https,443"` to wire up the gRPC protocol proxying. The API server should then be run with TLS disabled. Edit the `argocd-server` deployment to add the -`--insecure` flag to the argocd-server command: - -```yaml -spec: - template: - spec: - containers: - - name: argocd-server - command: - - /argocd-server - - --repo-server - - argocd-repo-server:8081 - - --insecure -``` +`--insecure` flag to the argocd-server command, or simply set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). ## [kubernetes/ingress-nginx](https://github.com/kubernetes/ingress-nginx) @@ -319,20 +306,7 @@ spec: ``` The API server should then be run with TLS disabled. Edit the `argocd-server` deployment to add the -`--insecure` flag to the argocd-server command: - -```yaml -spec: - template: - spec: - containers: - - name: argocd-server - command: - - argocd-server - - --repo-server - - argocd-repo-server:8081 - - --insecure -``` +`--insecure` flag to the argocd-server command, or simply set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). The obvious disadvantage to this approach is that this technique requires two separate hostnames for the API server -- one for gRPC and the other for HTTP/HTTPS. However it allows TLS termination to @@ -345,7 +319,7 @@ Traefik can be used as an edge router and provide [TLS](https://docs.traefik.io/ It currently has an advantage over NGINX in that it can terminate both TCP and HTTP connections _on the same port_ meaning you do not require multiple hosts or paths. -The API server should be run with TLS disabled. Edit the `argocd-server` deployment to add the `--insecure` flag to the argocd-server command. +The API server should be run with TLS disabled. Edit the `argocd-server` deployment to add the `--insecure` flag to the argocd-server command or set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). ### IngressRoute CRD ```yaml @@ -455,26 +429,9 @@ If you need detail for all the options available for these Google integrations, ### Disable internal TLS -First, to avoid internal redirection loops from HTTP to HTTPS, the API server should be run with TLS disabled. Edit the argocd-server deployment to add the --insecure flag to the argocd-server command. For this you can edit your resource live with `kubectl -n argocd edit deployments.apps argocd-server` or use a kustomize patch before installing Argo CD. - -The container command should change from: -```yaml - containers: - - command: - - argocd-server - - --staticassets - - /shared/app -``` +First, to avoid internal redirection loops from HTTP to HTTPS, the API server should be run with TLS disabled. -To: -```yaml - containers: - - command: - - argocd-server - - --insecure - - --staticassets - - /shared/app -``` +Edit the `--insecure` flag in the `argocd-server` command of the argocd-server deployment, or simply set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). ### Creating a service diff --git a/docs/operator-manual/installation.md b/docs/operator-manual/installation.md index 3b6f8f028f032..9255d138a17cb 100644 --- a/docs/operator-manual/installation.md +++ b/docs/operator-manual/installation.md @@ -32,7 +32,7 @@ Not recommended for production use. This type of installation is typically used > Note: Argo CD CRDs are not included into [namespace-install.yaml](https://github.com/argoproj/argo-cd/blob/master/manifests/namespace-install.yaml). > and have to be installed separately. The CRD manifests are located in the [manifests/crds](https://github.com/argoproj/argo-cd/blob/master/manifests/crds) directory. > Use the following command to install them: - > ```bash + > ``` > kubectl apply -k https://github.com/argoproj/argo-cd/manifests/crds\?ref\=stable > ``` @@ -81,3 +81,20 @@ resources: The Argo CD can be installed using [Helm](https://helm.sh/). The Helm chart is currently community maintained and available at [argo-helm/charts/argo-cd](https://github.com/argoproj/argo-helm/tree/master/charts/argo-cd). + +## Supported versions + +Similar to the Kubernetes project, the supported versions of Argo CD at any given point in time are the latest patch releases for the N +and N - 1 minor versions. +These Argo CD versions are supported on the same versions of Kubernetes that are supported by Kubernetes itself (normally the last 3 released versions). + +Essentially the Argo CD project follows the same support scheme as Kubernetes but for N, N-1 while Kubernetes supports N, N-1, N-2 versions. + +For example if the latest minor version of ArgoCD are 2.4.3 and 2.3.5 while supported Kubernetes versions are 1.24, 1.23 and 1.22 then the following combinations are supported: + +* Argo CD 2.4.3 on Kubernetes 1.24 +* Argo CD 2.4.3 on Kubernetes 1.23 +* Argo CD 2.4.3 on Kubernetes 1.22 +* Argo CD 2.3.5 on Kubernetes 1.24 +* Argo CD 2.3.5 on Kubernetes 1.23 +* Argo CD 2.3.5 on Kubernetes 1.22 \ No newline at end of file diff --git a/docs/operator-manual/metrics.md b/docs/operator-manual/metrics.md index 32ecbb83701aa..2753a01c39627 100644 --- a/docs/operator-manual/metrics.md +++ b/docs/operator-manual/metrics.md @@ -67,8 +67,10 @@ Scraped at the `argocd-server-metrics:8083/metrics` endpoint. | Metric | Type | Description | |--------|:----:|-------------| | `argocd_redis_request_duration` | histogram | Redis requests duration. | -| `argocd_redis_request_total` | counter | Number of kubernetes requests executed during application reconciliation. | - +| `argocd_redis_request_total` | counter | Number of kubernetes requests executed during application +reconciliation. | +| `grpc_server_handled_total` | counter | Total number of RPCs completed on the server, regardless of success or failure. | +| `grpc_server_msg_sent_total` | counter | Total number of gRPC stream messages sent by the server. | ## Repo Server Metrics Metrics about the Repo Server. Scraped at the `argocd-repo-server:8084/metrics` endpoint. diff --git a/docs/operator-manual/rbac.md b/docs/operator-manual/rbac.md index da4c9d5977871..ffade40b763c9 100644 --- a/docs/operator-manual/rbac.md +++ b/docs/operator-manual/rbac.md @@ -59,22 +59,7 @@ also use glob patterns in the action path: `action/*` (or regex patterns if you `exec` is a special resource. When enabled with the `create` action, this privilege allows a user to `exec` into Pods via the Argo CD UI. The functionality is similar to `kubectl exec`. -`exec` is a powerful privilege. It allows the user to run arbitrary code on any Pod managed by an Application for which -they have `create` privileges. If the Pod mounts a ServiceAccount token (which is the default behavior of Kubernetes), -then the user effectively has the same privileges as that ServiceAccount. - -The exec feature is disabled entirely by default. To enable it, set the `exec.enabled` key to "true" on the argocd-cm -ConfigMap. You will also need to add the following to the argocd-api-server Role (if you're using Argo CD in namespaced -mode) or ClusterRole (if you're using Argo CD in cluster mode). - -```yaml -- apiGroups: - - "" - resources: - - pods/exec - verbs: - - create -``` +See [Web-based Terminal](web_based_terminal.md) for more info. ## Tying It All Together diff --git a/docs/operator-manual/upgrading/2.1-2.2.md b/docs/operator-manual/upgrading/2.1-2.2.md index 35b3201c52be2..e2ca3dad17ad5 100644 --- a/docs/operator-manual/upgrading/2.1-2.2.md +++ b/docs/operator-manual/upgrading/2.1-2.2.md @@ -14,3 +14,76 @@ Note that bundled Helm has been upgraded from 3.6.0 to v3.7+. This includes foll - Experimental OCI support has been rewritten. More information in the [Helm v3.7.0 release notes](https://github.com/helm/helm/releases/tag/v3.7.0). + +## Support for private repo SSH keys using the SHA-1 signature hash algorithm is removed in 2.2.12 + +Argo CD 2.2.12 upgraded its base image from Ubuntu 21.10 to Ubuntu 22.04, which upgraded OpenSSH to 8.9. OpenSSH starting +with 8.8 [dropped support for the `ssh-rsa` SHA-1 key signature algorithm](https://www.openssh.com/txt/release-8.8). + +The signature algorithm is _not_ the same as the algorithm used when generating the key. There is no need to update +keys. + +The signature algorithm is negotiated with the SSH server when the connection is being set up. The client offers its +list of accepted signature algorithms, and if the server has a match, the connection proceeds. For most SSH servers on +up-to-date git providers, acceptable algorithms other than `ssh-rsa` should be available. + +Before upgrading to Argo CD 2.2.12, check whether your git provider(s) using SSH authentication support algorithms newer +than `rsa-ssh`. + +1. Make sure your version of SSH >= 8.9 (the version used by Argo CD). If not, upgrade it before proceeding. + + ```shell + ssh -V + ``` + + Example output: `OpenSSH_8.9p1 Ubuntu-3, OpenSSL 3.0.2 15 Mar 2022` + +2. Once you have a recent version of OpenSSH, follow the directions from the [OpenSSH 8.8 release notes](https://www.openssh.com/txt/release-8.7): + + > To check whether a server is using the weak ssh-rsa public key + > algorithm, for host authentication, try to connect to it after + > removing the ssh-rsa algorithm from ssh(1)'s allowed list: + > + > ```shell + > ssh -oHostKeyAlgorithms=-ssh-rsa user@host + > ``` + > + > If the host key verification fails and no other supported host key + > types are available, the server software on that host should be + > upgraded. + + If the server does not support an acceptable version, you will get an error similar to this; + + ``` + $ ssh -oHostKeyAlgorithms=-ssh-rsa vs-ssh.visualstudio.com + Unable to negotiate with 20.42.134.1 port 22: no matching host key type found. Their offer: ssh-rsa + ``` + + This indicates that the server needs to update its supported key signature algorithms, and Argo CD will not connect + to it. + +### Workaround + +The [OpenSSH 8.8 release notes](https://www.openssh.com/txt/release-8.8) describe a workaround if you cannot change the +server's key signature algorithms configuration. + +> Incompatibility is more likely when connecting to older SSH +> implementations that have not been upgraded or have not closely tracked +> improvements in the SSH protocol. For these cases, it may be necessary +> to selectively re-enable RSA/SHA1 to allow connection and/or user +> authentication via the HostkeyAlgorithms and PubkeyAcceptedAlgorithms +> options. For example, the following stanza in ~/.ssh/config will enable +> RSA/SHA1 for host and user authentication for a single destination host: +> +> ``` +> Host old-host +> HostkeyAlgorithms +ssh-rsa +> PubkeyAcceptedAlgorithms +ssh-rsa +> ``` +> +> We recommend enabling RSA/SHA1 only as a stopgap measure until legacy +> implementations can be upgraded or reconfigured with another key type +> (such as ECDSA or Ed25519). + +To apply this to Argo CD, you could create a ConfigMap with the desired ssh config file and then mount it at +`/home/argocd/.ssh/config`. diff --git a/docs/operator-manual/upgrading/2.2-2.3.md b/docs/operator-manual/upgrading/2.2-2.3.md index bde511f28cf3b..b7a1707e52cc3 100644 --- a/docs/operator-manual/upgrading/2.2-2.3.md +++ b/docs/operator-manual/upgrading/2.2-2.3.md @@ -46,3 +46,77 @@ Note that bundled Kustomize version has been upgraded from 4.2.0 to 4.4.1. ## Upgraded Helm Version Note that bundled Helm version has been upgraded from 3.7.1 to 3.8.0. + +## Support for private repo SSH keys using the SHA-1 signature hash algorithm is removed in 2.3.7 + +Argo CD 2.3.7 upgraded its base image from Ubuntu 21.04 to Ubuntu 22.04, which upgraded OpenSSH to 8.9. OpenSSH starting +with 8.8 [dropped support for the `ssh-rsa` SHA-1 key signature algorithm](https://www.openssh.com/txt/release-8.8). + +The signature algorithm is _not_ the same as the algorithm used when generating the key. There is no need to update +keys. + +The signature algorithm is negotiated with the SSH server when the connection is being set up. The client offers its +list of accepted signature algorithms, and if the server has a match, the connection proceeds. For most SSH servers on +up-to-date git providers, acceptable algorithms other than `ssh-rsa` should be available. + +Before upgrading to Argo CD 2.3.7, check whether your git provider(s) using SSH authentication support algorithms newer +than `rsa-ssh`. + +1. Make sure your version of SSH >= 8.9 (the version used by Argo CD). If not, upgrade it before proceeding. + + ```shell + ssh -V + ``` + + Example output: `OpenSSH_8.9p1 Ubuntu-3, OpenSSL 3.0.2 15 Mar 2022` + +2. Once you have a recent version of OpenSSH, follow the directions from the [OpenSSH 8.8 release notes](https://www.openssh.com/txt/release-8.7): + + > To check whether a server is using the weak ssh-rsa public key + > algorithm, for host authentication, try to connect to it after + > removing the ssh-rsa algorithm from ssh(1)'s allowed list: + > + > ```shell + > ssh -oHostKeyAlgorithms=-ssh-rsa user@host + > ``` + > + > If the host key verification fails and no other supported host key + > types are available, the server software on that host should be + > upgraded. + + If the server does not support an acceptable version, you will get an error similar to this; + + ``` + $ ssh -oHostKeyAlgorithms=-ssh-rsa vs-ssh.visualstudio.com + Unable to negotiate with 20.42.134.1 port 22: no matching host key type found. Their offer: ssh-rsa + ``` + + This indicates that the server needs to update its supported key signature algorithms, and Argo CD will not connect + to it. + +### Workaround + +The [OpenSSH 8.8 release notes](https://www.openssh.com/txt/release-8.8) describe a workaround if you cannot change the +server's key signature algorithms configuration. + +> Incompatibility is more likely when connecting to older SSH +> implementations that have not been upgraded or have not closely tracked +> improvements in the SSH protocol. For these cases, it may be necessary +> to selectively re-enable RSA/SHA1 to allow connection and/or user +> authentication via the HostkeyAlgorithms and PubkeyAcceptedAlgorithms +> options. For example, the following stanza in ~/.ssh/config will enable +> RSA/SHA1 for host and user authentication for a single destination host: +> +> ``` +> Host old-host +> HostkeyAlgorithms +ssh-rsa +> PubkeyAcceptedAlgorithms +ssh-rsa +> ``` +> +> We recommend enabling RSA/SHA1 only as a stopgap measure until legacy +> implementations can be upgraded or reconfigured with another key type +> (such as ECDSA or Ed25519). + +To apply this to Argo CD, you could create a ConfigMap with the desired ssh config file and then mount it at +`/home/argocd/.ssh/config`. + diff --git a/docs/operator-manual/upgrading/2.3-2.4.md b/docs/operator-manual/upgrading/2.3-2.4.md index d4d272b90ec1a..00869b790b06d 100644 --- a/docs/operator-manual/upgrading/2.3-2.4.md +++ b/docs/operator-manual/upgrading/2.3-2.4.md @@ -12,6 +12,8 @@ Helm 2 support was preserved in the Argo CD. We feel that Helm 3 is stable, and ## Support for private repo SSH keys using the SHA-1 signature hash algorithm is removed +Note: this change was back-ported to 2.3.7 and 2.2.12. + Argo CD 2.4 upgraded its base image from Ubuntu 20.04 to Ubuntu 22.04, which upgraded OpenSSH to 8.9. OpenSSH starting with 8.8 [dropped support for the `ssh-rsa` SHA-1 key signature algorithm](https://www.openssh.com/txt/release-8.8). @@ -105,13 +107,13 @@ p, role:org-admin, *, create, my-proj/*, allow New: ```csv -p, role: org-admin, clusters, create, my-proj/*, allow -p, role: org-admin, projects, create, my-proj/*, allow -p, role: org-admin, applications, create, my-proj/*, allow -p, role: org-admin, repositories, create, my-proj/*, allow -p, role: org-admin, certificates, create, my-proj/*, allow -p, role: org-admin, accounts, create, my-proj/*, allow -p, role: org-admin, gpgkeys, create, my-proj/*, allow +p, role:org-admin, clusters, create, my-proj/*, allow +p, role:org-admin, projects, create, my-proj/*, allow +p, role:org-admin, applications, create, my-proj/*, allow +p, role:org-admin, repositories, create, my-proj/*, allow +p, role:org-admin, certificates, create, my-proj/*, allow +p, role:org-admin, accounts, create, my-proj/*, allow +p, role:org-admin, gpgkeys, create, my-proj/*, allow ``` ## Enable logs RBAC enforcement @@ -149,9 +151,13 @@ p, role:test-db-admins, applications, *, staging-db-admins/*, allow p, role:test-db-admins, logs, get, staging-db-admins/*, allow ``` -## Known UI issue +### Pod Logs UI + +Since 2.4.9, the LOGS tab in pod view is visible in the UI only for users with explicit allow get logs policy. + +### Known pod logs UI issue prior to 2.4.9 -Currently, upon pressing the "LOGS" tab in pod view by users who don't have an explicit allow get logs policy, the red "unable to load data: Internal error" is received in the bottom of the screen, and "Failed to load data, please try again" is displayed. +Upon pressing the "LOGS" tab in pod view by users who don't have an explicit allow get logs policy, the red "unable to load data: Internal error" is received in the bottom of the screen, and "Failed to load data, please try again" is displayed. ## Test repo-server with its new dedicated Service Account diff --git a/docs/operator-manual/user-management/index.md b/docs/operator-manual/user-management/index.md index 186226495c954..7b24629367d36 100644 --- a/docs/operator-manual/user-management/index.md +++ b/docs/operator-manual/user-management/index.md @@ -496,3 +496,20 @@ data: clientSecret: $another-secret:oidc.auth0.clientSecret # Mind the ':' ... ``` + +### Skipping certificate verification on OIDC provider connections + +By default, all connections made by the API server to OIDC providers (either external providers or the bundled Dex +instance) must pass certificate validation. These connections occur when getting the OIDC provider's well-known +configuration, when getting the OIDC provider's keys, and when exchanging an authorization code or verifying an ID +token as part of an OIDC login flow. + +Disabling certificate verification might make sense if: +* You are using the bundled Dex instance **and** your Argo CD instance has TLS configured with a self-signed certificate + **and** you understand and accept the risks of skipping OIDC provider cert verification. +* You are using an external OIDC provider **and** that provider uses an invalid certificate **and** you cannot solve + the problem by setting `oidcConfig.rootCA` **and** you understand and accept the risks of skipping OIDC provider cert + verification. + +If either of those two applies, then you can disable OIDC provider certificate verification by setting +`oidc.tls.insecure.skip.verify` to `"true"` in the `argocd-cm` ConfigMap. diff --git a/docs/operator-manual/user-management/microsoft.md b/docs/operator-manual/user-management/microsoft.md index 1cf7931253891..04f06fafde720 100644 --- a/docs/operator-manual/user-management/microsoft.md +++ b/docs/operator-manual/user-management/microsoft.md @@ -36,24 +36,24 @@ 1. Edit `argocd-cm` and add the following `dex.config` to the data section, replacing the `caData`, `my-argo-cd-url` and `my-login-url` your values from the Azure AD App: data: - url: https://my-argo-cd-url - dex.config: | - logger: + url: https://my-argo-cd-url + dex.config: | + logger: level: debug format: json - connectors: - - type: saml + connectors: + - type: saml id: saml name: saml config: - entityIssuer: https://my-argo-cd-url/api/dex/callback - ssoURL: https://my-login-url (e.g. https://login.microsoftonline.com/xxxxx/a/saml2) - caData: | - MY-BASE64-ENCODED-CERTIFICATE-DATA - redirectURI: https://my-argo-cd-url/api/dex/callback - usernameAttr: email - emailAttr: email - groupsAttr: Group + entityIssuer: https://my-argo-cd-url/api/dex/callback + ssoURL: https://my-login-url (e.g. https://login.microsoftonline.com/xxxxx/a/saml2) + caData: | + MY-BASE64-ENCODED-CERTIFICATE-DATA + redirectURI: https://my-argo-cd-url/api/dex/callback + usernameAttr: email + emailAttr: email + groupsAttr: Group 2. Edit `argocd-rbac-cm` to configure permissions, similar to example below. - Use Azure AD `Group IDs` for assigning roles. @@ -169,7 +169,7 @@ p, role:org-admin, repositories, update, *, allow p, role:org-admin, repositories, delete, *, allow g, "84ce98d1-e359-4f3b-85af-985b458de3c6", role:org-admin - scopes: '[roles, email]' + scopes: '[groups, email]' Refer to [operator-manual/argocd-rbac-cm.yaml](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-rbac-cm.yaml) for all of the available variables. diff --git a/docs/operator-manual/web_based_terminal.md b/docs/operator-manual/web_based_terminal.md new file mode 100644 index 0000000000000..25fccf2de4195 --- /dev/null +++ b/docs/operator-manual/web_based_terminal.md @@ -0,0 +1,45 @@ +# Web-based Terminal + +![Argo CD Terminal](../assets/terminal.png) + +Since v2.4, Argo CD has a web-based terminal that allows you to get a shell inside a running pod just like you would with +`kubectl exec`. It's basically SSH from your browser, full ANSI color support and all! However, for security this feature +is disabled by default. + +This is a powerful privilege. It allows the user to run arbitrary code on any Pod managed by an Application for which +they have the `exec/create` privilege. If the Pod mounts a ServiceAccount token (which is the default behavior of +Kubernetes), then the user effectively has the same privileges as that ServiceAccount. + +## Enabling the terminal + +1. Set the `exec.enabled` key to `"true"` on the `argocd-cm` ConfigMap. + +2. Patch the `argocd-server` Role (if using namespaced Argo) or ClusterRole (if using clustered Argo) to allow `argocd-server` +to exec into pods +```yaml +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create +``` + +3. Add RBAC rules to allow your users to `create` the `exec` resource, i.e. +``` +p, role:myrole, exec, create, */*, allow +``` + +See [RBAC Configuration](rbac.md#exec-resource) for more info. + +## Changing allowed shells + +By default, Argo CD attempts to execute shells in this order: + +1. bash +2. sh +3. powershell +4. cmd + +If none of the shells are found, the terminal session will fail. To add to or change the allowed shells, change the +`exec.shells` key in the `argocd-cm` ConfigMap, separating them with commas. \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 50064668f8dbb..bc395d2b3a0bd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,5 @@ mkdocs==1.2.3 mkdocs-material==7.1.7 markdown_include==0.6.0 pygments==2.7.4 -jinja2===3.0.3 \ No newline at end of file +jinja2==3.0.3 +markdown==3.3.7 \ No newline at end of file diff --git a/docs/user-guide/config-management-plugins.md b/docs/user-guide/config-management-plugins.md index f50646272c982..d54c812590b7d 100644 --- a/docs/user-guide/config-management-plugins.md +++ b/docs/user-guide/config-management-plugins.md @@ -14,7 +14,7 @@ There are two ways to install a Config Management Plugin (CMP): 1. Add the plugin config to the Argo CD ConfigMap. The repo-server container will run your plugin's commands. This is a good option for a simple plugin that requires only a few lines of code that fit nicely in the Argo CD ConfigMap. 2. Add the plugin as a sidecar to the repo-server Pod. - This is a good option for a more complex plugin that would clutter the Argo CD ConfigMap. + This is a good option for a more complex plugin that would clutter the Argo CD ConfigMap. A copy of the repository is sent to the sidecar container as a tarball and processed individually per application, which makes it a good option for [concurrent processing of monorepos](../operator-manual/high_availability.md#enable-concurrent-processing). ### Option 1: Configure plugins via Argo CD configmap @@ -235,16 +235,17 @@ If you don't need to set any environment variables, you can set an empty plugin is 90s. So if you increase the repo server timeout greater than 90s, be sure to set `ARGOCD_EXEC_TIMEOUT` on the sidecar. -## Tarball stream filtering +## Plugin tar stream exclusions In order to increase the speed of manifest generation, certain files and folders can be excluded from being sent to your plugin. We recommend excluding your `.git` folder if it isn't necessary. Use Go's [filepatch.Match](https://pkg.go.dev/path/filepath#Match) syntax. You can set it one of three ways: + 1. The `--plugin-tar-exclude` argument on the repo server. 2. The `reposerver.plugin.tar.exclusions` key if you are using `argocd-cmd-params-cm` -3. Directly setting 'ARGOCD_REPO_SERVER_PLUGIN_TAR_EXCLUSIONS' environment variable on the repo server. +3. Directly setting `ARGOCD_REPO_SERVER_PLUGIN_TAR_EXCLUSIONS` environment variable on the repo server. For option 1, the flag can be repeated multiple times. For option 2 and 3, you can specify multiple globs by separating them with semicolons. \ No newline at end of file diff --git a/docs/user-guide/diffing.md b/docs/user-guide/diffing.md index 4f48ab592924a..4ff91fe3f31db 100644 --- a/docs/user-guide/diffing.md +++ b/docs/user-guide/diffing.md @@ -81,7 +81,7 @@ data: - '.webhooks[]?.clientConfig.caBundle' ``` -Resource customization can also be configured to ignore all differences made by a managedField.manager at the system level. The example bellow shows how to configure ArgoCD to ignore changes made by `kube-controller-manager` in `Deployment` resources. +Resource customization can also be configured to ignore all differences made by a managedField.manager at the system level. The example bellow shows how to configure Argo CD to ignore changes made by `kube-controller-manager` in `Deployment` resources. ```yaml data: @@ -90,7 +90,7 @@ data: - kube-controller-manager ``` -It is possible to configure ignoreDifferences to be applied to all resources in every Application managed by an ArgoCD instance. In order to do so, resource customizations can be configured like in the example bellow: +It is possible to configure ignoreDifferences to be applied to all resources in every Application managed by an Argo CD instance. In order to do so, resource customizations can be configured like in the example bellow: ```yaml data: @@ -116,11 +116,26 @@ data: By default `status` field is ignored during diffing for `CustomResourceDefinition` resource. The behavior can be extended to all resources using `all` value or disabled using `none`. +### Ignoring RBAC changes made by AggregateRoles + +If you are using [Aggregated ClusterRoles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles) and don't want Argo CD to detect the `rules` changes as drift, you can set `resource.compareoptions.ignoreAggregatedRoles: true`. Then Argo CD will no longer detect these changes as an event that requires syncing. + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cm +data: + resource.compareoptions: | + # disables status field diffing in specified resource types + ignoreAggregatedRoles: true +``` + ## Known Kubernetes types in CRDs (Resource limits, Volume mounts etc) Some CRDs are re-using data structures defined in the Kubernetes source base and therefore inheriting custom JSON/YAML marshaling. Custom marshalers might serialize CRDs in a slightly different format that causes false -positives during drift detection. +positives during drift detection. A typical example is the `argoproj.io/Rollout` CRD that re-using `core/v1/PodSpec` data structure. Pod resource requests might be reformatted by the custom marshaller of `IntOrString` data type: @@ -140,7 +155,7 @@ resources: ``` The solution is to specify which CRDs fields are using built-in Kubernetes types in the `resource.customizations` -section of `argocd-cm` ConfigMap: +section of `argocd-cm` ConfigMap: ```yaml apiVersion: v1 diff --git a/docs/user-guide/kustomize.md b/docs/user-guide/kustomize.md index 0b62a0ff6c8f3..e377c9886b91a 100644 --- a/docs/user-guide/kustomize.md +++ b/docs/user-guide/kustomize.md @@ -85,3 +85,22 @@ argocd app set --kustomize-version v3.5.4 ## Build Environment Kustomize does not support parameters and therefore cannot support the standard [build environment](build-environment.md). + +## Kustomizing Helm charts + +It's possible to [render Helm charts with Kustomize](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/chart.md). +Doing so requires that you pass the `--enable-helm` flag to the `kustomize build` command. +This flag is not part of the Kustomize options within Argo CD. +If you would like to render Helm charts through Kustomize in an Argo CD application, you have two options: +You can either create a [custom plugin](https://argo-cd.readthedocs.io/en/stable/user-guide/config-management-plugins/), or modify the `argocd-cm` ConfigMap to include the `--enable-helm` flag globally for all Kustomize applications: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cm + namespace: argocd +data: + kustomize.buildOptions: --enable-helm +``` + diff --git a/docs/user-guide/sync-options.md b/docs/user-guide/sync-options.md index 208f405628607..bad6cece42ebc 100644 --- a/docs/user-guide/sync-options.md +++ b/docs/user-guide/sync-options.md @@ -187,3 +187,17 @@ spec: ``` The example above shows how an ArgoCD Application can be configured so it will ignore the `spec.replicas` field from the desired state (git) during the sync stage. This is achieve by calculating and pre-patching the desired state before applying it in the cluster. Note that the `RespectIgnoreDifferences` sync option is only effective when the resource is already created in the cluster. If the Application is being created and no live state exists, the desired state is applied as-is. + +## Create Namespace + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + namespace: test +spec: + syncPolicy: + syncOptions: + - CreateNamespace=true +``` +The example above shows how an Argo CD Application can be configured so it will create namespaces for the Application resources if the namespaces don't exist already. Without this either declared in the Application manifest or passed in the cli via `--sync-option CreateNamespace=true`, the Application will fail to sync if the resources' namespaces do not exist. diff --git a/go.mod b/go.mod index 54b3762caed30..e3761bf72e5c8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis v2.5.0+incompatible github.com/alicebob/miniredis/v2 v2.14.2 - github.com/argoproj/gitops-engine v0.7.0 + github.com/argoproj/gitops-engine v0.7.3 github.com/argoproj/notifications-engine v0.3.1-0.20220430155844-567361917320 github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0 github.com/aws/aws-sdk-go v1.38.49 @@ -268,6 +268,9 @@ replace ( github.com/grpc-ecosystem/grpc-gateway => github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/improbable-eng/grpc-web => github.com/improbable-eng/grpc-web v0.0.0-20181111100011-16092bd1d58a + // Avoid CVE-2022-28948 + gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 + // https://github.com/kubernetes/kubernetes/issues/79384#issuecomment-505627280 k8s.io/api => k8s.io/api v0.23.1 k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.1 diff --git a/go.sum b/go.sum index 9ec4a914aec98..b9bbe010e9028 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,8 @@ github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/appscode/go v0.0.0-20190808133642-1d4ef1f1c1e0/go.mod h1:iy07dV61Z7QQdCKJCIvUoDL21u6AIceRhZzyleh2ymc= -github.com/argoproj/gitops-engine v0.7.0 h1:X6W8VP9bWTe74wWxAV3i8KZ0yBmre5DU8g+GWH09FCo= -github.com/argoproj/gitops-engine v0.7.0/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c= +github.com/argoproj/gitops-engine v0.7.3 h1:0ZlRTReAJG5Y1PviQ8ZIJq/+VowxWe2uFwoXqYcbtXU= +github.com/argoproj/gitops-engine v0.7.3/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c= github.com/argoproj/notifications-engine v0.3.1-0.20220430155844-567361917320 h1:XDjtTfccs4rSOT1n+i1zV9RpxQdKky1b4YBic16E0qY= github.com/argoproj/notifications-engine v0.3.1-0.20220430155844-567361917320/go.mod h1:R3zlopt+/juYlebQc9Jarn9vBQ2xZruWOWjUNkfGY9M= github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0 h1:Cfp7rO/HpVxnwlRqJe0jHiBbZ77ZgXhB6HWlYD02Xdc= @@ -1726,10 +1726,8 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/manifests/base/dex/argocd-dex-server-deployment.yaml b/manifests/base/dex/argocd-dex-server-deployment.yaml index 66c70b536e4b8..f2a96a4d843f6 100644 --- a/manifests/base/dex/argocd-dex-server-deployment.yaml +++ b/manifests/base/dex/argocd-dex-server-deployment.yaml @@ -35,7 +35,7 @@ spec: runAsNonRoot: true containers: - name: dex - image: ghcr.io/dexidp/dex:v2.30.2 + image: ghcr.io/dexidp/dex:v2.32.0 imagePullPolicy: Always command: [/shared/argocd-dex, rundex] securityContext: diff --git a/manifests/base/kustomization.yaml b/manifests/base/kustomization.yaml index 89b860acd1705..3574cb6f0ff56 100644 --- a/manifests/base/kustomization.yaml +++ b/manifests/base/kustomization.yaml @@ -5,7 +5,7 @@ kind: Kustomization images: - name: quay.io/argoproj/argocd newName: quay.io/argoproj/argocd - newTag: v2.4.4 + newTag: v2.4.11 resources: - ./application-controller - ./dex diff --git a/manifests/base/redis/argocd-redis-deployment.yaml b/manifests/base/redis/argocd-redis-deployment.yaml index a2e4dd7455f0e..9f75c5a3215a2 100644 --- a/manifests/base/redis/argocd-redis-deployment.yaml +++ b/manifests/base/redis/argocd-redis-deployment.yaml @@ -21,7 +21,7 @@ spec: serviceAccountName: argocd-redis containers: - name: redis - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: Always args: - "--save" diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml index 40311ebcf5bfb..9d2bd74317582 100644 --- a/manifests/core-install.yaml +++ b/manifests/core-install.yaml @@ -9385,7 +9385,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: argocd-applicationset-controller ports: @@ -9465,7 +9465,7 @@ spec: - "" - --appendonly - "no" - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: Always name: redis ports: @@ -9615,7 +9615,7 @@ spec: value: /helm-working-dir - name: HELM_DATA_HOME value: /helm-working-dir - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: failureThreshold: 3 @@ -9664,7 +9664,7 @@ spec: - -n - /usr/local/bin/argocd - /var/run/argocd/argocd-cmp-server - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 name: copyutil securityContext: allowPrivilegeEscalation: false @@ -9851,7 +9851,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: diff --git a/manifests/core-install/kustomization.yaml b/manifests/core-install/kustomization.yaml index ae7750d680f4f..3424546894fbc 100644 --- a/manifests/core-install/kustomization.yaml +++ b/manifests/core-install/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: quay.io/argoproj/argocd newName: quay.io/argoproj/argocd - newTag: v2.4.4 + newTag: v2.4.11 diff --git a/manifests/ha/base/kustomization.yaml b/manifests/ha/base/kustomization.yaml index 9105ba02154e7..e4549764aa385 100644 --- a/manifests/ha/base/kustomization.yaml +++ b/manifests/ha/base/kustomization.yaml @@ -11,7 +11,7 @@ patchesStrategicMerge: images: - name: quay.io/argoproj/argocd newName: quay.io/argoproj/argocd - newTag: v2.4.4 + newTag: v2.4.11 resources: - ../../base/application-controller - ../../base/applicationset-controller diff --git a/manifests/ha/base/redis-ha/chart/upstream.yaml b/manifests/ha/base/redis-ha/chart/upstream.yaml index 244492c98bcd9..2a75fb9cf8249 100644 --- a/manifests/ha/base/redis-ha/chart/upstream.yaml +++ b/manifests/ha/base/redis-ha/chart/upstream.yaml @@ -770,7 +770,7 @@ spec: topologyKey: kubernetes.io/hostname initContainers: - name: config-init - image: haproxy:2.0.25-alpine + image: haproxy:2.0.29-alpine imagePullPolicy: IfNotPresent resources: {} @@ -790,7 +790,7 @@ spec: runAsUser: 1000 containers: - name: haproxy - image: haproxy:2.0.25-alpine + image: haproxy:2.0.29-alpine imagePullPolicy: IfNotPresent livenessProbe: httpGet: @@ -878,7 +878,7 @@ spec: automountServiceAccountToken: false initContainers: - name: config-init - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent resources: {} @@ -906,7 +906,7 @@ spec: containers: - name: redis - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent command: - redis-server @@ -947,7 +947,7 @@ spec: lifecycle: {} - name: sentinel - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent command: - redis-sentinel diff --git a/manifests/ha/base/redis-ha/chart/values.yaml b/manifests/ha/base/redis-ha/chart/values.yaml index 0a5f690d08b97..9123ab7db988f 100644 --- a/manifests/ha/base/redis-ha/chart/values.yaml +++ b/manifests/ha/base/redis-ha/chart/values.yaml @@ -9,12 +9,12 @@ redis-ha: haproxy: enabled: true image: - tag: 2.0.25-alpine + tag: 2.0.29-alpine timeout: server: 6m client: 6m checkInterval: 3s image: - tag: 7.0.0-alpine + tag: 7.0.4-alpine sentinel: bind: "0.0.0.0" diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index be9798cc7f08d..d975c2963fff9 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -10320,7 +10320,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: argocd-applicationset-controller ports: @@ -10392,7 +10392,7 @@ spec: - command: - /shared/argocd-dex - rundex - image: ghcr.io/dexidp/dex:v2.30.2 + image: ghcr.io/dexidp/dex:v2.32.0 imagePullPolicy: Always name: dex ports: @@ -10417,7 +10417,7 @@ spec: - -n - /usr/local/bin/argocd - /shared/argocd-dex - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: copyutil securityContext: @@ -10457,7 +10457,7 @@ spec: containers: - command: - argocd-notifications - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: tcpSocket: @@ -10526,7 +10526,7 @@ spec: app.kubernetes.io/name: argocd-redis-ha-haproxy topologyKey: kubernetes.io/hostname containers: - - image: haproxy:2.0.25-alpine + - image: haproxy:2.0.29-alpine imagePullPolicy: IfNotPresent lifecycle: {} livenessProbe: @@ -10555,7 +10555,7 @@ spec: - /readonly/haproxy_init.sh command: - sh - image: haproxy:2.0.25-alpine + image: haproxy:2.0.29-alpine imagePullPolicy: IfNotPresent name: config-init volumeMounts: @@ -10714,7 +10714,7 @@ spec: value: /helm-working-dir - name: HELM_DATA_HOME value: /helm-working-dir - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: failureThreshold: 3 @@ -10763,7 +10763,7 @@ spec: - -n - /usr/local/bin/argocd - /var/run/argocd/argocd-cmp-server - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 name: copyutil securityContext: allowPrivilegeEscalation: false @@ -11010,7 +11010,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: @@ -11218,7 +11218,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: @@ -11299,7 +11299,7 @@ spec: - /data/conf/redis.conf command: - redis-server - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent lifecycle: {} livenessProbe: @@ -11337,7 +11337,7 @@ spec: - /data/conf/sentinel.conf command: - redis-sentinel - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent lifecycle: {} livenessProbe: @@ -11383,7 +11383,7 @@ spec: value: 40000915ab58c3fa8fd888fb8b24711944e6cbb4 - name: SENTINEL_ID_2 value: 2bbec7894d954a8af3bb54d13eaec53cb024e2ca - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent name: config-init volumeMounts: diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml index 330aacb64277f..28bc603563f7d 100644 --- a/manifests/ha/namespace-install.yaml +++ b/manifests/ha/namespace-install.yaml @@ -1244,7 +1244,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: argocd-applicationset-controller ports: @@ -1316,7 +1316,7 @@ spec: - command: - /shared/argocd-dex - rundex - image: ghcr.io/dexidp/dex:v2.30.2 + image: ghcr.io/dexidp/dex:v2.32.0 imagePullPolicy: Always name: dex ports: @@ -1341,7 +1341,7 @@ spec: - -n - /usr/local/bin/argocd - /shared/argocd-dex - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: copyutil securityContext: @@ -1381,7 +1381,7 @@ spec: containers: - command: - argocd-notifications - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: tcpSocket: @@ -1450,7 +1450,7 @@ spec: app.kubernetes.io/name: argocd-redis-ha-haproxy topologyKey: kubernetes.io/hostname containers: - - image: haproxy:2.0.25-alpine + - image: haproxy:2.0.29-alpine imagePullPolicy: IfNotPresent lifecycle: {} livenessProbe: @@ -1479,7 +1479,7 @@ spec: - /readonly/haproxy_init.sh command: - sh - image: haproxy:2.0.25-alpine + image: haproxy:2.0.29-alpine imagePullPolicy: IfNotPresent name: config-init volumeMounts: @@ -1638,7 +1638,7 @@ spec: value: /helm-working-dir - name: HELM_DATA_HOME value: /helm-working-dir - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: failureThreshold: 3 @@ -1687,7 +1687,7 @@ spec: - -n - /usr/local/bin/argocd - /var/run/argocd/argocd-cmp-server - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 name: copyutil securityContext: allowPrivilegeEscalation: false @@ -1934,7 +1934,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: @@ -2142,7 +2142,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: @@ -2223,7 +2223,7 @@ spec: - /data/conf/redis.conf command: - redis-server - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent lifecycle: {} livenessProbe: @@ -2261,7 +2261,7 @@ spec: - /data/conf/sentinel.conf command: - redis-sentinel - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent lifecycle: {} livenessProbe: @@ -2307,7 +2307,7 @@ spec: value: 40000915ab58c3fa8fd888fb8b24711944e6cbb4 - name: SENTINEL_ID_2 value: 2bbec7894d954a8af3bb54d13eaec53cb024e2ca - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: IfNotPresent name: config-init volumeMounts: diff --git a/manifests/install.yaml b/manifests/install.yaml index f09da2db86158..70f18584dbf4d 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -9692,7 +9692,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: argocd-applicationset-controller ports: @@ -9764,7 +9764,7 @@ spec: - command: - /shared/argocd-dex - rundex - image: ghcr.io/dexidp/dex:v2.30.2 + image: ghcr.io/dexidp/dex:v2.32.0 imagePullPolicy: Always name: dex ports: @@ -9789,7 +9789,7 @@ spec: - -n - /usr/local/bin/argocd - /shared/argocd-dex - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: copyutil securityContext: @@ -9829,7 +9829,7 @@ spec: containers: - command: - argocd-notifications - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: tcpSocket: @@ -9904,7 +9904,7 @@ spec: - "" - --appendonly - "no" - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: Always name: redis ports: @@ -10054,7 +10054,7 @@ spec: value: /helm-working-dir - name: HELM_DATA_HOME value: /helm-working-dir - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: failureThreshold: 3 @@ -10103,7 +10103,7 @@ spec: - -n - /usr/local/bin/argocd - /var/run/argocd/argocd-cmp-server - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 name: copyutil securityContext: allowPrivilegeEscalation: false @@ -10346,7 +10346,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: @@ -10548,7 +10548,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index 01488a5b3a951..ddb29ae344047 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -616,7 +616,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: argocd-applicationset-controller ports: @@ -688,7 +688,7 @@ spec: - command: - /shared/argocd-dex - rundex - image: ghcr.io/dexidp/dex:v2.30.2 + image: ghcr.io/dexidp/dex:v2.32.0 imagePullPolicy: Always name: dex ports: @@ -713,7 +713,7 @@ spec: - -n - /usr/local/bin/argocd - /shared/argocd-dex - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always name: copyutil securityContext: @@ -753,7 +753,7 @@ spec: containers: - command: - argocd-notifications - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: tcpSocket: @@ -828,7 +828,7 @@ spec: - "" - --appendonly - "no" - image: redis:7.0.0-alpine + image: redis:7.0.4-alpine imagePullPolicy: Always name: redis ports: @@ -978,7 +978,7 @@ spec: value: /helm-working-dir - name: HELM_DATA_HOME value: /helm-working-dir - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: failureThreshold: 3 @@ -1027,7 +1027,7 @@ spec: - -n - /usr/local/bin/argocd - /var/run/argocd/argocd-cmp-server - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 name: copyutil securityContext: allowPrivilegeEscalation: false @@ -1270,7 +1270,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: @@ -1472,7 +1472,7 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true - image: quay.io/argoproj/argocd:v2.4.4 + image: quay.io/argoproj/argocd:v2.4.11 imagePullPolicy: Always livenessProbe: httpGet: diff --git a/mkdocs.yml b/mkdocs.yml index edc1eb9437d26..9468aa57fe1c3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ nav: - operator-manual/custom_tools.md - operator-manual/custom-styles.md - operator-manual/metrics.md + - operator-manual/web_based_terminal.md - Notification: - Overview: operator-manual/notifications/index.md - operator-manual/notifications/triggers.md @@ -160,6 +161,7 @@ nav: - developer-guide/releasing.md - developer-guide/site.md - developer-guide/static-code-analysis.md + - developer-guide/ui-extensions.md - developer-guide/faq.md - faq.md - security_considerations.md diff --git a/pkg/apis/application/v1alpha1/repository_types.go b/pkg/apis/application/v1alpha1/repository_types.go index 21e43286292df..e5f6971648949 100644 --- a/pkg/apis/application/v1alpha1/repository_types.go +++ b/pkg/apis/application/v1alpha1/repository_types.go @@ -195,30 +195,40 @@ func (repo *Repository) GetHelmCreds() helm.Creds { } func getCAPath(repoURL string) string { - hostname := "" + // For git ssh protocol url without ssh://, url.Parse() will fail to parse. + // However, no warn log is output since ssh scheme url is a possible format. + if ok, _ := git.IsSSHURL(repoURL); ok { + return "" + } + hostname := "" // url.Parse() will happily parse most things thrown at it. When the URL // is either https or oci, we use the parsed hostname to receive the cert, // otherwise we'll use the parsed path (OCI repos are often specified as // hostname, without protocol). - if parsedURL, err := url.Parse(repoURL); err == nil { - if parsedURL.Scheme == "https" || parsedURL.Scheme == "oci" { - hostname = parsedURL.Host - } else if parsedURL.Scheme == "" { - hostname = parsedURL.Path - } - } else { + parsedURL, err := url.Parse(repoURL) + if err != nil { log.Warnf("Could not parse repo URL '%s': %v", repoURL, err) + return "" + } + if parsedURL.Scheme == "https" || parsedURL.Scheme == "oci" { + hostname = parsedURL.Host + } else if parsedURL.Scheme == "" { + hostname = parsedURL.Path } - if hostname != "" { - if caPath, err := cert.GetCertBundlePathForRepository(hostname); err == nil { - return caPath - } else { - log.Warnf("Could not get cert bundle path for repository '%s': %v", repoURL, err) - } + if hostname == "" { + log.Warnf("Could not get hostname for repository '%s'", repoURL) + return "" + } + + caPath, err := cert.GetCertBundlePathForRepository(hostname) + if err != nil { + log.Warnf("Could not get cert bundle path for repository '%s': %v", repoURL, err) + return "" } - return "" + + return caPath } // CopySettingsFrom copies all repository settings from source to receiver diff --git a/pkg/apis/application/v1alpha1/types_test.go b/pkg/apis/application/v1alpha1/types_test.go index a06facf59debf..a68fc019366c1 100644 --- a/pkg/apis/application/v1alpha1/types_test.go +++ b/pkg/apis/application/v1alpha1/types_test.go @@ -2628,6 +2628,7 @@ func TestGetCAPath(t *testing.T) { "oci://bar.example.com", "bar.example.com", "ssh://foo.example.com", + "git@example.com:organization/reponame.git", "/some/invalid/thing", "../another/invalid/thing", "./also/invalid", diff --git a/reposerver/server.go b/reposerver/server.go index a6f5e3b04bdc2..b7cf4f9537a5f 100644 --- a/reposerver/server.go +++ b/reposerver/server.go @@ -3,10 +3,11 @@ package reposerver import ( "crypto/tls" "fmt" - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "os" "path/filepath" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" @@ -15,6 +16,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/keepalive" "google.golang.org/grpc/reflection" "github.com/argoproj/argo-cd/v2/common" @@ -86,6 +88,11 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(streamInterceptors...)), grpc.MaxRecvMsgSize(apiclient.MaxGRPCMessageSize), grpc.MaxSendMsgSize(apiclient.MaxGRPCMessageSize), + grpc.KeepaliveEnforcementPolicy( + keepalive.EnforcementPolicy{ + MinTime: common.GRPCKeepAliveEnforcementMinimum, + }, + ), } // We do allow for non-TLS servers to be created, in case of mTLS will be diff --git a/server/application/terminal.go b/server/application/terminal.go index 6cbbab81b0bd9..94645b53feb6c 100644 --- a/server/application/terminal.go +++ b/server/application/terminal.go @@ -33,17 +33,19 @@ type terminalHandler struct { enf *rbac.Enforcer cache *servercache.Cache appResourceTreeFn func(ctx context.Context, app *appv1.Application) (*appv1.ApplicationTree, error) + allowedShells []string } // NewHandler returns a new terminal handler. func NewHandler(appLister applisters.ApplicationNamespaceLister, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache, - appResourceTree AppResourceTreeFn) *terminalHandler { + appResourceTree AppResourceTreeFn, allowedShells []string) *terminalHandler { return &terminalHandler{ appLister: appLister, db: db, enf: enf, cache: cache, appResourceTreeFn: appResourceTree, + allowedShells: allowedShells, } } @@ -123,7 +125,7 @@ func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Namespace name is not valid", http.StatusBadRequest) return } - shell := q.Get("shell") // No need to validate. Will only buse used if it's in the allow-list. + shell := q.Get("shell") // No need to validate. Will only be used if it's in the allow-list. ctx := r.Context() @@ -216,14 +218,12 @@ func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer session.Done() - validShells := []string{"bash", "sh", "powershell", "cmd"} - if isValidShell(validShells, shell) { + if isValidShell(s.allowedShells, shell) { cmd := []string{shell} err = startProcess(kubeClientset, config, namespace, podName, container, cmd, session) } else { - // No shell given or it was not valid: try some shells until one succeeds or all fail - // FIXME: if the first shell fails then the first keyboard event is lost - for _, testShell := range validShells { + // No shell given or the given shell was not allowed: try the configured shells until one succeeds or all fail. + for _, testShell := range s.allowedShells { cmd := []string{testShell} if err = startProcess(kubeClientset, config, namespace, podName, container, cmd, session); err == nil { break diff --git a/server/application/websocket.go b/server/application/websocket.go index cf2c4db288910..ff72aa28644da 100644 --- a/server/application/websocket.go +++ b/server/application/websocket.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "sync" "time" "github.com/gorilla/websocket" @@ -22,10 +23,12 @@ var upgrader = func() websocket.Upgrader { // terminalSession implements PtyHandler type terminalSession struct { - wsConn *websocket.Conn - sizeChan chan remotecommand.TerminalSize - doneChan chan struct{} - tty bool + wsConn *websocket.Conn + sizeChan chan remotecommand.TerminalSize + doneChan chan struct{} + tty bool + readLock sync.Mutex + writeLock sync.Mutex } // newTerminalSession create terminalSession @@ -60,7 +63,9 @@ func (t *terminalSession) Next() *remotecommand.TerminalSize { // Read called in a loop from remotecommand as long as the process is running func (t *terminalSession) Read(p []byte) (int, error) { + t.readLock.Lock() _, message, err := t.wsConn.ReadMessage() + t.readLock.Unlock() if err != nil { log.Errorf("read message err: %v", err) return copy(p, EndOfTransmission), err @@ -91,7 +96,10 @@ func (t *terminalSession) Write(p []byte) (int, error) { log.Errorf("write parse message err: %v", err) return 0, err } - if err := t.wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + t.writeLock.Lock() + err = t.wsConn.WriteMessage(websocket.TextMessage, msg) + t.writeLock.Unlock() + if err != nil { log.Errorf("write message err: %v", err) return 0, err } diff --git a/server/server.go b/server/server.go index ce76b6f119815..036f8614e9df9 100644 --- a/server/server.go +++ b/server/server.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + goio "io" "io/fs" "math" "net" @@ -12,6 +13,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "reflect" "regexp" go_runtime "runtime" @@ -22,6 +24,8 @@ import ( // nolint:staticcheck golang_proto "github.com/golang/protobuf/proto" + netCtx "context" + "github.com/argoproj/pkg/sync" "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v4" @@ -35,11 +39,11 @@ import ( log "github.com/sirupsen/logrus" "github.com/soheilhy/cmux" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - netCtx "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" "google.golang.org/grpc/metadata" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" @@ -98,6 +102,7 @@ import ( "github.com/argoproj/argo-cd/v2/util/healthz" httputil "github.com/argoproj/argo-cd/v2/util/http" "github.com/argoproj/argo-cd/v2/util/io" + "github.com/argoproj/argo-cd/v2/util/io/files" jwtutil "github.com/argoproj/argo-cd/v2/util/jwt" kubeutil "github.com/argoproj/argo-cd/v2/util/kube" "github.com/argoproj/argo-cd/v2/util/oidc" @@ -608,6 +613,11 @@ func (a *ArgoCDServer) newGRPCServer() (*grpc.Server, application.AppResourceTre grpc.MaxRecvMsgSize(apiclient.MaxGRPCMessageSize), grpc.MaxSendMsgSize(apiclient.MaxGRPCMessageSize), grpc.ConnectionTimeout(300 * time.Second), + grpc.KeepaliveEnforcementPolicy( + keepalive.EnforcementPolicy{ + MinTime: common.GRPCKeepAliveEnforcementMinimum, + }, + ), } sensitiveMethods := map[string]bool{ "/cluster.ClusterService/Create": true, @@ -802,7 +812,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl } mux.Handle("/api/", handler) - terminalHandler := application.NewHandler(a.appLister, a.db, a.enf, a.Cache, appResourceTreeFn) + terminalHandler := application.NewHandler(a.appLister, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells) mux.HandleFunc("/terminal", func(writer http.ResponseWriter, request *http.Request) { argocdSettings, err := a.settingsMgr.GetSettings() if err != nil { @@ -865,11 +875,11 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl registerDownloadHandlers(mux, "/download") // Serve extensions - var extensionsApiPath = "/extensions/" var extensionsSharedPath = "/tmp/extensions/" - extHandler := http.StripPrefix(extensionsApiPath, http.FileServer(http.Dir(extensionsSharedPath))) - mux.HandleFunc(extensionsApiPath, extHandler.ServeHTTP) + mux.HandleFunc("/extensions.js", func(writer http.ResponseWriter, _ *http.Request) { + a.serveExtensions(extensionsSharedPath, writer) + }) // Serve UI static assets var assetsHandler http.Handler = http.HandlerFunc(a.newStaticAssetsHandler()) @@ -880,6 +890,48 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl return &httpS } +var extensionsPattern = regexp.MustCompile(`^extension(.*)\.js$`) + +func (a *ArgoCDServer) serveExtensions(extensionsSharedPath string, w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/javascript") + + err := filepath.Walk(extensionsSharedPath, func(filePath string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed to iterate files in '%s': %w", extensionsSharedPath, err) + } + if !files.IsSymlink(info) && !info.IsDir() && extensionsPattern.MatchString(info.Name()) { + processFile := func() error { + if _, err = w.Write([]byte(fmt.Sprintf("// source: %s/%s \n", filePath, info.Name()))); err != nil { + return fmt.Errorf("failed to write to response: %w", err) + } + + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file '%s': %w", filePath, err) + } + defer io.Close(f) + + if _, err := goio.Copy(w, f); err != nil { + return fmt.Errorf("failed to copy file '%s': %w", filePath, err) + } + + return nil + } + + if processFile() != nil { + return fmt.Errorf("failed to serve extension file '%s': %w", filePath, processFile()) + } + } + return nil + }) + + if err != nil && !os.IsNotExist(err) { + log.Errorf("Failed to walk extensions directory: %v", err) + http.Error(w, "Internal error", http.StatusInternalServerError) + return + } +} + // registerDexHandlers will register dex HTTP handlers, creating the the OAuth client app func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) { if !a.settings.IsSSOConfigured() { diff --git a/test/container/Dockerfile b/test/container/Dockerfile index 011f3913ff705..9122f650bc27c 100644 --- a/test/container/Dockerfile +++ b/test/container/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.0.0 as redis +FROM redis:7.0.4 as redis FROM node:12.18.4-buster as node diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index 116e0c9b55f97..b71db2917fbf9 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -38,6 +39,7 @@ import ( projectFixture "github.com/argoproj/argo-cd/v2/test/e2e/fixture/project" repoFixture "github.com/argoproj/argo-cd/v2/test/e2e/fixture/repos" "github.com/argoproj/argo-cd/v2/test/e2e/testdata" + "github.com/argoproj/argo-cd/v2/util/argo" . "github.com/argoproj/argo-cd/v2/util/argo" . "github.com/argoproj/argo-cd/v2/util/errors" "github.com/argoproj/argo-cd/v2/util/io" @@ -947,7 +949,7 @@ func TestLocalManifestSync(t *testing.T) { And(func(app *Application) { res, _ := RunCli("app", "manifests", app.Name) assert.Contains(t, res, "containerPort: 80") - assert.Contains(t, res, "image: gcr.io/heptio-images/ks-guestbook-demo:0.2") + assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.2") }). Given(). LocalPath(guestbookPathLocal). @@ -958,7 +960,7 @@ func TestLocalManifestSync(t *testing.T) { And(func(app *Application) { res, _ := RunCli("app", "manifests", app.Name) assert.Contains(t, res, "containerPort: 81") - assert.Contains(t, res, "image: gcr.io/heptio-images/ks-guestbook-demo:0.3") + assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.3") }). Given(). LocalPath(""). @@ -969,7 +971,7 @@ func TestLocalManifestSync(t *testing.T) { And(func(app *Application) { res, _ := RunCli("app", "manifests", app.Name) assert.Contains(t, res, "containerPort: 80") - assert.Contains(t, res, "image: gcr.io/heptio-images/ks-guestbook-demo:0.2") + assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.2") }) } @@ -1190,7 +1192,8 @@ func TestPermissionWithScopedRepo(t *testing.T) { Name(projName). Destination("*,*"). When(). - Create() + Create(). + AddSource("*") repoFixture.Given(t, true). When(). @@ -1598,9 +1601,11 @@ func TestSyncWithInfos(t *testing.T) { }) } -//Given: argocd app create does not provide --dest-namespace -// Manifest contains resource console which does not require namespace -//Expect: no app.Status.Conditions +// Given: argocd app create does not provide --dest-namespace +// +// Manifest contains resource console which does not require namespace +// +// Expect: no app.Status.Conditions func TestCreateAppWithNoNameSpaceForGlobalResource(t *testing.T) { Given(t). Path(globalWithNoNameSpace). @@ -1615,10 +1620,12 @@ func TestCreateAppWithNoNameSpaceForGlobalResource(t *testing.T) { }) } -//Given: argocd app create does not provide --dest-namespace -// Manifest contains resource deployment, and service which requires namespace -// Deployment and service do not have namespace in manifest -//Expect: app.Status.Conditions for deployment ans service which does not have namespace in manifest +// Given: argocd app create does not provide --dest-namespace +// +// Manifest contains resource deployment, and service which requires namespace +// Deployment and service do not have namespace in manifest +// +// Expect: app.Status.Conditions for deployment ans service which does not have namespace in manifest func TestCreateAppWithNoNameSpaceWhenRequired(t *testing.T) { Given(t). Path(guestbookPath). @@ -1636,11 +1643,13 @@ func TestCreateAppWithNoNameSpaceWhenRequired(t *testing.T) { }) } -//Given: argocd app create does not provide --dest-namespace -// Manifest contains resource deployment, and service which requires namespace -// Some deployment and service has namespace in manifest -// Some deployment and service does not have namespace in manifest -//Expect: app.Status.Conditions for deployment and service which does not have namespace in manifest +// Given: argocd app create does not provide --dest-namespace +// +// Manifest contains resource deployment, and service which requires namespace +// Some deployment and service has namespace in manifest +// Some deployment and service does not have namespace in manifest +// +// Expect: app.Status.Conditions for deployment and service which does not have namespace in manifest func TestCreateAppWithNoNameSpaceWhenRequired2(t *testing.T) { Given(t). Path(guestbookWithNamespace). @@ -1716,10 +1725,13 @@ func TestListResource(t *testing.T) { } // Given application is set with --sync-option CreateNamespace=true -// application --dest-namespace does not exist +// +// application --dest-namespace does not exist +// // Verity application --dest-namespace is created -// application sync successful -// when application is deleted, --dest-namespace is not deleted +// +// application sync successful +// when application is deleted, --dest-namespace is not deleted func TestNamespaceAutoCreation(t *testing.T) { SkipOnEnv(t, "OPENSHIFT") updatedNamespace := getNewNamespace(t) @@ -2063,3 +2075,313 @@ func TestDisableManifestGeneration(t *testing.T) { assert.Equal(t, app.Status.SourceType, ApplicationSourceTypeDirectory) }) } + +func TestSwitchTrackingMethod(t *testing.T) { + ctx := Given(t) + + ctx. + SetTrackingMethod(string(argo.TrackingMethodAnnotation)). + Path("deployment"). + When(). + CreateApp(). + Sync(). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add resource with tracking annotation. This should put the + // application OutOfSync. + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-configmap", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/other-configmap", Name(), DeploymentNamespace()), + }, + }, + }, metav1.CreateOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Delete resource to bring application back in sync + FailOnErr(nil, KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Delete(context.Background(), "other-configmap", metav1.DeleteOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + SetTrackingMethod(string(argo.TrackingMethodLabel)). + Sync(). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add a resource with a tracking annotation. This should not + // affect the application, because we now use the tracking method + // "label". + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-configmap", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/other-configmap", Name(), DeploymentNamespace()), + }, + }, + }, metav1.CreateOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add a resource with the tracking label. The app should become + // OutOfSync. + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "extra-configmap", + Labels: map[string]string{ + common.LabelKeyAppInstance: Name(), + }, + }, + }, metav1.CreateOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Delete resource to bring application back in sync + FailOnErr(nil, KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Delete(context.Background(), "extra-configmap", metav1.DeleteOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)) +} + +func TestSwitchTrackingLabel(t *testing.T) { + ctx := Given(t) + + ctx. + Path("deployment"). + When(). + CreateApp(). + Sync(). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add extra resource that carries the default tracking label + // We expect the app to go out of sync. + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-configmap", + Labels: map[string]string{ + common.LabelKeyAppInstance: Name(), + }, + }, + }, metav1.CreateOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Delete resource to bring application back in sync + FailOnErr(nil, KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Delete(context.Background(), "other-configmap", metav1.DeleteOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + // Change tracking label + SetTrackingLabel("argocd.tracking"). + Sync(). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Create resource with the new tracking label, the application + // is expected to go out of sync + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-configmap", + Labels: map[string]string{ + "argocd.tracking": Name(), + }, + }, + }, metav1.CreateOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Delete resource to bring application back in sync + FailOnErr(nil, KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Delete(context.Background(), "other-configmap", metav1.DeleteOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add extra resource that carries the default tracking label + // We expect the app to stay in sync, because the configured + // label is different. + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-configmap", + Labels: map[string]string{ + common.LabelKeyAppInstance: Name(), + }, + }, + }, metav1.CreateOptions{})) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)) +} + +func TestAnnotationTrackingExtraResources(t *testing.T) { + ctx := Given(t) + + SetTrackingMethod(string(argo.TrackingMethodAnnotation)) + ctx. + Path("deployment"). + When(). + CreateApp(). + Sync(). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add a resource with an annotation that is not referencing the + // resource. + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "extra-configmap", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: fmt.Sprintf("%s:apps/Deployment:%s/guestbook-cm", Name(), DeploymentNamespace()), + }, + }, + }, metav1.CreateOptions{})) + }). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + Sync("--prune"). + And(func() { + // The extra configmap must not be pruned, because it's not tracked + cm, err := KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Get(context.Background(), "extra-configmap", metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "extra-configmap", cm.Name) + }). + And(func() { + // Add a resource with an annotation that is self-referencing the + // resource. + FailOnErr(KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-configmap", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: fmt.Sprintf("%s:/ConfigMap:%s/other-configmap", Name(), DeploymentNamespace()), + }, + }, + }, metav1.CreateOptions{})) + }). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + Sync("--prune"). + And(func() { + // The extra configmap must be pruned now, because it's tracked + cm, err := KubeClientset.CoreV1().ConfigMaps(DeploymentNamespace()).Get(context.Background(), "other-configmap", metav1.GetOptions{}) + require.Error(t, err) + require.Equal(t, "", cm.Name) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add a cluster-scoped resource that is not referencing itself + FailOnErr(KubeClientset.RbacV1().ClusterRoles().Create(context.Background(), &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-test-clusterrole", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: fmt.Sprintf("%s:rbac.authorization.k8s.io/ClusterRole:%s/e2e-other-clusterrole", Name(), DeploymentNamespace()), + }, + Labels: map[string]string{ + fixture.TestingLabel: "true", + }, + }, + }, metav1.CreateOptions{})) + }). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + And(func() { + // Add a cluster-scoped resource that is referencing itself + FailOnErr(KubeClientset.RbacV1().ClusterRoles().Create(context.Background(), &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-other-clusterrole", + Annotations: map[string]string{ + common.AnnotationKeyAppInstance: fmt.Sprintf("%s:rbac.authorization.k8s.io/ClusterRole:%s/e2e-other-clusterrole", Name(), DeploymentNamespace()), + }, + Labels: map[string]string{ + fixture.TestingLabel: "true", + }, + }, + }, metav1.CreateOptions{})) + }). + Refresh(RefreshTypeNormal). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + Sync("--prune"). + And(func() { + // The extra configmap must be pruned now, because it's tracked and does not exist in git + cr, err := KubeClientset.RbacV1().ClusterRoles().Get(context.Background(), "e2e-other-clusterrole", metav1.GetOptions{}) + require.Error(t, err) + require.Equal(t, "", cr.Name) + }). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)) + +} diff --git a/test/e2e/diff_test.go b/test/e2e/diff_test.go index 9849cb7e0b1a1..feb295d9c09be 100644 --- a/test/e2e/diff_test.go +++ b/test/e2e/diff_test.go @@ -22,7 +22,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" @@ -43,7 +43,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/test/e2e/fixture/app/actions.go b/test/e2e/fixture/app/actions.go index 283529f08cb13..7b49192562f40 100644 --- a/test/e2e/fixture/app/actions.go +++ b/test/e2e/fixture/app/actions.go @@ -350,3 +350,13 @@ func (a *Actions) verifyAction() { a.Then().Expect(Success("")) } } + +func (a *Actions) SetTrackingMethod(trackingMethod string) *Actions { + fixture.SetTrackingMethod(trackingMethod) + return a +} + +func (a *Actions) SetTrackingLabel(trackingLabel string) *Actions { + fixture.SetTrackingLabel(trackingLabel) + return a +} diff --git a/test/e2e/fixture/app/context.go b/test/e2e/fixture/app/context.go index fafff85a4c916..a527405ea6aee 100644 --- a/test/e2e/fixture/app/context.go +++ b/test/e2e/fixture/app/context.go @@ -308,3 +308,8 @@ func (c *Context) HelmSkipCrds() *Context { c.helmSkipCrds = true return c } + +func (c *Context) SetTrackingMethod(trackingMethod string) *Context { + fixture.SetTrackingMethod(trackingMethod) + return c +} diff --git a/test/e2e/fixture/applicationsets/actions.go b/test/e2e/fixture/applicationsets/actions.go index e0eb562091fef..5ac62e2a01057 100644 --- a/test/e2e/fixture/applicationsets/actions.go +++ b/test/e2e/fixture/applicationsets/actions.go @@ -14,11 +14,12 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/kubernetes" + "github.com/argoproj/argo-cd/v2/common" argocommon "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1" "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils" + "github.com/argoproj/argo-cd/v2/util/clusterauth" ) // this implements the "when" part of given/when/then @@ -61,42 +62,6 @@ func (a *Actions) Then() *Consequences { return &Consequences{a.context, a} } -// GetServiceAccountBearerToken will attempt to get the provided service account until it -// exists, iterate the secrets associated with it looking for one of type -// kubernetes.io/service-account-token, and return it's token if found. -// (function based on 'GetServiceAccountBearerToken' from Argo CD's 'clusterauth.go') -func GetServiceAccountBearerToken(clientset kubernetes.Interface, ns string, sa string) (string, error) { - var serviceAccount *corev1.ServiceAccount - var secret *corev1.Secret - var err error - err = wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) { - serviceAccount, err = clientset.CoreV1().ServiceAccounts(ns).Get(context.Background(), sa, metav1.GetOptions{}) - if err != nil { - return false, err - } - // Scan all secrets looking for one of the correct type: - for _, oRef := range serviceAccount.Secrets { - var getErr error - secret, err = clientset.CoreV1().Secrets(ns).Get(context.Background(), oRef.Name, metav1.GetOptions{}) - if err != nil { - return false, fmt.Errorf("failed to retrieve secret %q: %v", oRef.Name, getErr) - } - if secret.Type == corev1.SecretTypeServiceAccountToken { - return true, nil - } - } - return false, nil - }) - if err != nil { - return "", fmt.Errorf("failed to wait for service account secret: %v", err) - } - token, ok := secret.Data["token"] - if !ok { - return "", fmt.Errorf("secret %q for service account %q did not have a token", secret.Name, serviceAccount) - } - return string(token), nil -} - // CreateClusterSecret creates a faux cluster secret, with the given cluster server and cluster name (this cluster // will not actually be used by the Argo CD controller, but that's not needed for our E2E tests) func (a *Actions) CreateClusterSecret(secretName string, clusterName string, clusterServer string) *Actions { @@ -135,7 +100,7 @@ func (a *Actions) CreateClusterSecret(secretName string, clusterName string, clu if err == nil { var bearerToken string - bearerToken, err = GetServiceAccountBearerToken(fixtureClient.KubeClientset, utils.ArgoCDNamespace, serviceAccountName) + bearerToken, err = clusterauth.GetServiceAccountBearerToken(fixtureClient.KubeClientset, utils.ArgoCDNamespace, serviceAccountName, common.BearerTokenTimeout) // bearerToken secret := &corev1.Secret{ diff --git a/test/e2e/fixture/fixture.go b/test/e2e/fixture/fixture.go index 3f736f572a405..b5afd9b9ee199 100644 --- a/test/e2e/fixture/fixture.go +++ b/test/e2e/fixture/fixture.go @@ -42,7 +42,7 @@ const ( defaultAdminPassword = "password" defaultAdminUsername = "admin" DefaultTestUserPassword = "password" - testingLabel = "e2e.argoproj.io" + TestingLabel = "e2e.argoproj.io" ArgoCDNamespace = "argocd-e2e" // ensure all repos are in one directory tree, so we can easily clean them up @@ -162,7 +162,8 @@ func init() { adminUsername = GetEnvWithDefault(EnvAdminUsername, defaultAdminUsername) AdminPassword = GetEnvWithDefault(EnvAdminPassword, defaultAdminPassword) - tlsTestResult, err := grpcutil.TestTLS(apiServerAddress) + dialTime := 30 * time.Second + tlsTestResult, err := grpcutil.TestTLS(apiServerAddress, dialTime) CheckError(err) ArgoCDClientset, err = apiclient.NewClient(&apiclient.ClientOptions{Insecure: true, ServerAddr: apiServerAddress, PlainText: !tlsTestResult.TLS}) @@ -304,7 +305,7 @@ func CreateSecret(username, password string) string { "--from-literal=username="+username, "--from-literal=password="+password, "-n", TestNamespace())) - FailOnErr(Run("", "kubectl", "label", "secret", secretName, testingLabel+"=true", "-n", TestNamespace())) + FailOnErr(Run("", "kubectl", "label", "secret", secretName, TestingLabel+"=true", "-n", TestNamespace())) return secretName } @@ -363,6 +364,13 @@ func SetTrackingMethod(trackingMethod string) { }) } +func SetTrackingLabel(trackingLabel string) { + updateSettingConfigMap(func(cm *corev1.ConfigMap) error { + cm.Data["application.instanceLabelKey"] = trackingLabel + return nil + }) +} + func SetResourceOverridesSplitKeys(overrides map[string]v1alpha1.ResourceOverride) { updateSettingConfigMap(func(cm *corev1.ConfigMap) error { for k, v := range overrides { @@ -519,10 +527,11 @@ func EnsureCleanState(t *testing.T) { v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeCluster})) // kubectl delete secrets -l e2e.argoproj.io=true CheckError(KubeClientset.CoreV1().Secrets(TestNamespace()).DeleteCollection(context.Background(), - v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{LabelSelector: testingLabel + "=true"})) + v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{LabelSelector: TestingLabel + "=true"})) - FailOnErr(Run("", "kubectl", "delete", "ns", "-l", testingLabel+"=true", "--field-selector", "status.phase=Active", "--wait=false")) - FailOnErr(Run("", "kubectl", "delete", "crd", "-l", testingLabel+"=true", "--wait=false")) + FailOnErr(Run("", "kubectl", "delete", "ns", "-l", TestingLabel+"=true", "--field-selector", "status.phase=Active", "--wait=false")) + FailOnErr(Run("", "kubectl", "delete", "crd", "-l", TestingLabel+"=true", "--wait=false")) + FailOnErr(Run("", "kubectl", "delete", "clusterroles", "-l", TestingLabel+"=true", "--wait=false")) // reset settings updateSettingConfigMap(func(cm *corev1.ConfigMap) error { @@ -613,7 +622,7 @@ func EnsureCleanState(t *testing.T) { // create namespace FailOnErr(Run("", "kubectl", "create", "ns", DeploymentNamespace())) - FailOnErr(Run("", "kubectl", "label", "ns", DeploymentNamespace(), testingLabel+"=true")) + FailOnErr(Run("", "kubectl", "label", "ns", DeploymentNamespace(), TestingLabel+"=true")) log.WithFields(log.Fields{"duration": time.Since(start), "name": t.Name(), "id": id, "username": "admin", "password": "password"}).Info("clean state") } diff --git a/test/e2e/fixture/project/context.go b/test/e2e/fixture/project/context.go index 360e81155d500..95f3749c7bfe3 100644 --- a/test/e2e/fixture/project/context.go +++ b/test/e2e/fixture/project/context.go @@ -15,6 +15,7 @@ type Context struct { timeout int name string destination string + repos []string } func Given(t *testing.T) *Context { @@ -43,6 +44,11 @@ func (c *Context) Destination(destination string) *Context { return c } +func (c *Context) SourceRepositories(repos []string) *Context { + c.repos = repos + return c +} + func (c *Context) And(block func()) *Context { block() return c diff --git a/test/e2e/hook_test.go b/test/e2e/hook_test.go index 9623b85ba6561..f6bb1be872ac6 100644 --- a/test/e2e/hook_test.go +++ b/test/e2e/hook_test.go @@ -171,7 +171,7 @@ spec: containers: - command: - "true" - image: "alpine:latest" + image: "quay.io/argoprojlabs/argocd-e2e-container:0.1" imagePullPolicy: IfNotPresent name: main restartPolicy: Never @@ -202,7 +202,7 @@ spec: containers: - command: - "true" - image: "alpine:latest" + image: "quay.io/argoprojlabs/argocd-e2e-container:0.1" imagePullPolicy: IfNotPresent name: main restartPolicy: Never @@ -218,7 +218,7 @@ spec: containers: - command: - "false" - image: "alpine:latest" + image: "quay.io/argoprojlabs/argocd-e2e-container:0.1" imagePullPolicy: IfNotPresent name: main restartPolicy: Never diff --git a/test/e2e/multiarch-container/Dockerfile b/test/e2e/multiarch-container/Dockerfile new file mode 100644 index 0000000000000..41667d28f8176 --- /dev/null +++ b/test/e2e/multiarch-container/Dockerfile @@ -0,0 +1,2 @@ +FROM docker.io/library/busybox +CMD exec sh -c "trap : TERM INT; echo 'Hi' && tail -f /dev/null" diff --git a/test/e2e/multiarch-container/build.sh b/test/e2e/multiarch-container/build.sh new file mode 100755 index 0000000000000..350f6e30d548f --- /dev/null +++ b/test/e2e/multiarch-container/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -x +VERSIONS="0.1 0.2 0.3" +PLATFORMS="linux/amd64,linux/arm64,linux/s390x,linux/ppc64le" +for version in $VERSIONS; do + docker buildx build \ + -t "quay.io/argoprojlabs/argocd-e2e-container:${version}" \ + --platform "${PLATFORMS}" \ + --push \ + . +done diff --git a/test/e2e/testdata/cluster-role/pod.yaml b/test/e2e/testdata/cluster-role/pod.yaml index cb488d7feba96..efc927693e07c 100644 --- a/test/e2e/testdata/cluster-role/pod.yaml +++ b/test/e2e/testdata/cluster-role/pod.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/test/e2e/testdata/crd-validation/deployment.yaml b/test/e2e/testdata/crd-validation/deployment.yaml index 2b9fcaf354bea..92890c1a90986 100644 --- a/test/e2e/testdata/crd-validation/deployment.yaml +++ b/test/e2e/testdata/crd-validation/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: nginx - image: nginx:1.17.4-alpine + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 ports: - containerPort: "80" imagePullPolicy: IfNotPresent \ No newline at end of file diff --git a/test/e2e/testdata/deployment/deployment.yaml b/test/e2e/testdata/deployment/deployment.yaml index d84ca5e1f785f..92e22a2ba9841 100644 --- a/test/e2e/testdata/deployment/deployment.yaml +++ b/test/e2e/testdata/deployment/deployment.yaml @@ -16,6 +16,6 @@ spec: spec: containers: - name: nginx - image: nginx:1.17.4-alpine + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 ports: - containerPort: 80 diff --git a/test/e2e/testdata/deprecated-extensions/deployment.yaml b/test/e2e/testdata/deprecated-extensions/deployment.yaml index b82457347dadf..5593c3157fc57 100644 --- a/test/e2e/testdata/deprecated-extensions/deployment.yaml +++ b/test/e2e/testdata/deprecated-extensions/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: extensions-deployment - image: "gcr.io/heptio-images/ks-guestbook-demo:0.2" + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent ports: - name: http diff --git a/test/e2e/testdata/git-submodule/submodule-pod.yaml b/test/e2e/testdata/git-submodule/submodule-pod.yaml index 1f43fe9dd9f9e..fa3b92c2f5875 100644 --- a/test/e2e/testdata/git-submodule/submodule-pod.yaml +++ b/test/e2e/testdata/git-submodule/submodule-pod.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/test/e2e/testdata/guestbook-logs/guestbook-ui-deployment.yaml b/test/e2e/testdata/guestbook-logs/guestbook-ui-deployment.yaml index fe1f59e312acb..592d552fa70a4 100644 --- a/test/e2e/testdata/guestbook-logs/guestbook-ui-deployment.yaml +++ b/test/e2e/testdata/guestbook-logs/guestbook-ui-deployment.yaml @@ -14,13 +14,8 @@ spec: app: guestbook-ui spec: containers: - - image: busybox + - image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent name: guestbook-ui - command: - - sh - args: - - -c - - "echo \"Hi\" && tail -f /dev/null" ports: - containerPort: 80 diff --git a/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment-ns.yaml b/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment-ns.yaml index 08751d3cc8d24..dba58068db39c 100644 --- a/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment-ns.yaml +++ b/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment-ns.yaml @@ -15,7 +15,7 @@ spec: app: guestbook-ui spec: containers: - - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 + - image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent name: guestbook-ui ports: diff --git a/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment.yaml b/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment.yaml index 434505e7462d0..039508bab8e24 100644 --- a/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment.yaml +++ b/test/e2e/testdata/guestbook-with-namespace/guestbook-ui-deployment.yaml @@ -14,7 +14,7 @@ spec: app: guestbook-ui spec: containers: - - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 + - image: quay.io/argoprojlabs/argocd-e2e-container:0.2 imagePullPolicy: IfNotPresent name: guestbook-ui ports: diff --git a/test/e2e/testdata/guestbook/guestbook-ui-deployment.yaml b/test/e2e/testdata/guestbook/guestbook-ui-deployment.yaml index 434505e7462d0..039508bab8e24 100644 --- a/test/e2e/testdata/guestbook/guestbook-ui-deployment.yaml +++ b/test/e2e/testdata/guestbook/guestbook-ui-deployment.yaml @@ -14,7 +14,7 @@ spec: app: guestbook-ui spec: containers: - - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 + - image: quay.io/argoprojlabs/argocd-e2e-container:0.2 imagePullPolicy: IfNotPresent name: guestbook-ui ports: diff --git a/test/e2e/testdata/guestbook_local/guestbook-ui-deployment.yaml b/test/e2e/testdata/guestbook_local/guestbook-ui-deployment.yaml index 27a269561d602..d3c08e97c911e 100644 --- a/test/e2e/testdata/guestbook_local/guestbook-ui-deployment.yaml +++ b/test/e2e/testdata/guestbook_local/guestbook-ui-deployment.yaml @@ -14,7 +14,7 @@ spec: app: guestbook-ui spec: containers: - - image: gcr.io/heptio-images/ks-guestbook-demo:0.3 + - image: quay.io/argoprojlabs/argocd-e2e-container:0.3 name: guestbook-ui ports: - containerPort: 81 diff --git a/test/e2e/testdata/hook-and-deployment/deployment.yaml b/test/e2e/testdata/hook-and-deployment/deployment.yaml index d10b14eb6398f..d16a3a6b4d3ba 100644 --- a/test/e2e/testdata/hook-and-deployment/deployment.yaml +++ b/test/e2e/testdata/hook-and-deployment/deployment.yaml @@ -14,7 +14,7 @@ spec: spec: containers: - name: main - image: nginx:1.17.4-alpine + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent readinessProbe: failureThreshold: 3 diff --git a/test/e2e/testdata/hook-and-deployment/hook.yaml b/test/e2e/testdata/hook-and-deployment/hook.yaml index a852381d27a50..86af9d881b0a7 100644 --- a/test/e2e/testdata/hook-and-deployment/hook.yaml +++ b/test/e2e/testdata/hook-and-deployment/hook.yaml @@ -10,7 +10,7 @@ spec: containers: - command: - "true" - image: "alpine:3.10.2" + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent name: main restartPolicy: Never \ No newline at end of file diff --git a/test/e2e/testdata/hook/hook.yaml b/test/e2e/testdata/hook/hook.yaml index c14b9d24d9adb..ca89c3d6d5bb6 100644 --- a/test/e2e/testdata/hook/hook.yaml +++ b/test/e2e/testdata/hook/hook.yaml @@ -8,7 +8,7 @@ spec: containers: - command: - "true" - image: "alpine:latest" + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent name: main restartPolicy: Never \ No newline at end of file diff --git a/test/e2e/testdata/hook/pod.yaml b/test/e2e/testdata/hook/pod.yaml index 938a0ff19ac2b..001aa52b04cec 100644 --- a/test/e2e/testdata/hook/pod.yaml +++ b/test/e2e/testdata/hook/pod.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: main - image: alpine:latest + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/test/e2e/testdata/kustomize/pod.yaml b/test/e2e/testdata/kustomize/pod.yaml index fa4d920c8b8d4..7b701b46448a6 100644 --- a/test/e2e/testdata/kustomize/pod.yaml +++ b/test/e2e/testdata/kustomize/pod.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/test/e2e/testdata/multi-namespace-hook/multi-namespace-hook.yaml b/test/e2e/testdata/multi-namespace-hook/multi-namespace-hook.yaml index 90979745ff5c9..2a4cb9f5a60f2 100644 --- a/test/e2e/testdata/multi-namespace-hook/multi-namespace-hook.yaml +++ b/test/e2e/testdata/multi-namespace-hook/multi-namespace-hook.yaml @@ -22,7 +22,7 @@ spec: spec: containers: - name: nginx - image: nginx:1.17.4-alpine + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent ports: - containerPort: 80 @@ -39,7 +39,7 @@ spec: restartPolicy: Never containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: [sh, -c, "sleep 10"] @@ -56,6 +56,6 @@ spec: restartPolicy: Never containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: [sh, -c, "sleep 10"] diff --git a/test/e2e/testdata/multi-namespace/deployment-with-namespace.yaml b/test/e2e/testdata/multi-namespace/deployment-with-namespace.yaml index 9b5023622e806..474e117ac487c 100644 --- a/test/e2e/testdata/multi-namespace/deployment-with-namespace.yaml +++ b/test/e2e/testdata/multi-namespace/deployment-with-namespace.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: helm-guestbook - image: "gcr.io/heptio-images/ks-guestbook-demo:0.2" + image: quay.io/argoprojlabs/argocd-e2e-container:0.2 imagePullPolicy: IfNotPresent ports: - name: http @@ -30,4 +30,4 @@ spec: readinessProbe: httpGet: path: / - port: http \ No newline at end of file + port: http diff --git a/test/e2e/testdata/multi-namespace/deployment.yaml b/test/e2e/testdata/multi-namespace/deployment.yaml index d9bed73131f11..08d30228fad03 100644 --- a/test/e2e/testdata/multi-namespace/deployment.yaml +++ b/test/e2e/testdata/multi-namespace/deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: helm-guestbook - image: "gcr.io/heptio-images/ks-guestbook-demo:0.2" + image: quay.io/argoprojlabs/argocd-e2e-container:0.2 imagePullPolicy: IfNotPresent ports: - name: http diff --git a/test/e2e/testdata/networking/guestbook-ui-deployment.yaml b/test/e2e/testdata/networking/guestbook-ui-deployment.yaml index de0f2851efc63..69869e4103ac1 100644 --- a/test/e2e/testdata/networking/guestbook-ui-deployment.yaml +++ b/test/e2e/testdata/networking/guestbook-ui-deployment.yaml @@ -14,7 +14,7 @@ spec: app: guestbook-ui spec: containers: - - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 + - image: quay.io/argoprojlabs/argocd-e2e-container:0.2 name: guestbook-ui ports: - containerPort: 80 diff --git a/test/e2e/testdata/one-deployment/deployment.yaml b/test/e2e/testdata/one-deployment/deployment.yaml index 4aaaa9141b7f0..9ddf4a4bcd372 100644 --- a/test/e2e/testdata/one-deployment/deployment.yaml +++ b/test/e2e/testdata/one-deployment/deployment.yaml @@ -16,6 +16,6 @@ spec: containers: - name: main command: ["sleep", "999"] - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent diff --git a/test/e2e/testdata/two-nice-pods/pod-1.yaml b/test/e2e/testdata/two-nice-pods/pod-1.yaml index 31f73520f5498..95187dd42227d 100644 --- a/test/e2e/testdata/two-nice-pods/pod-1.yaml +++ b/test/e2e/testdata/two-nice-pods/pod-1.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/test/e2e/testdata/two-nice-pods/pod-2.yaml b/test/e2e/testdata/two-nice-pods/pod-2.yaml index d7a493015dcc6..588d281e5fd81 100644 --- a/test/e2e/testdata/two-nice-pods/pod-2.yaml +++ b/test/e2e/testdata/two-nice-pods/pod-2.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: main - image: alpine:3.10.2 + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 imagePullPolicy: IfNotPresent command: - "true" diff --git a/ui/src/app/applications/components/application-summary/application-summary.tsx b/ui/src/app/applications/components/application-summary/application-summary.tsx index da4bd94ca115d..a166d3b1b9b45 100644 --- a/ui/src/app/applications/components/application-summary/application-summary.tsx +++ b/ui/src/app/applications/components/application-summary/application-summary.tsx @@ -13,6 +13,7 @@ import {RevisionFormField} from '../revision-form-field/revision-form-field'; import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage, urlPattern} from '../utils'; import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options'; import {ApplicationRetryView} from '../application-retry-view/application-retry-view'; +import {Link} from 'react-router-dom'; require('./application-summary.scss'); @@ -31,7 +32,7 @@ export const ApplicationSummary = (props: {app: models.Application; updateApp: ( const attributes = [ { title: 'PROJECT', - view: {app.spec.project}, + view: {app.spec.project}, edit: (formApi: FormApi) => ( services.projects.list('items.metadata.name').then(projs => projs.map(item => item.metadata.name))}> {projects => } diff --git a/ui/src/app/applications/components/resource-details/resource-details.tsx b/ui/src/app/applications/components/resource-details/resource-details.tsx index 3c1b4acf3c1c3..871abb7b47ebc 100644 --- a/ui/src/app/applications/components/resource-details/resource-details.tsx +++ b/ui/src/app/applications/components/resource-details/resource-details.tsx @@ -1,5 +1,4 @@ import {DataLoader, Tab, Tabs} from 'argo-ui'; -import {useData} from 'argo-ui/v2'; import * as React from 'react'; import {EventsList, YamlEditor} from '../../../shared/components'; import * as models from '../../../shared/models'; @@ -7,7 +6,7 @@ import {ErrorBoundary} from '../../../shared/components/error-boundary/error-bou import {Context} from '../../../shared/context'; import {Application, ApplicationTree, AppSourceType, Event, RepoAppDetails, ResourceNode, State, SyncStatuses} from '../../../shared/models'; import {services} from '../../../shared/services'; -import {ExtensionComponentProps} from '../../../shared/services/extensions-service'; +import {ResourceTabExtension} from '../../../shared/services/extensions-service'; import {NodeInfo, SelectNode} from '../application-details/application-details'; import {ApplicationNodeInfo} from '../application-node-info/application-node-info'; import {ApplicationParameters} from '../application-parameters/application-parameters'; @@ -48,9 +47,11 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { state: State, podState: State, events: Event[], - ExtensionComponent: React.ComponentType, + extensionTabs: ResourceTabExtension[], tabs: Tab[], - execEnabled: boolean + execEnabled: boolean, + execAllowed: boolean, + logsAllowed: boolean ) => { if (!node || node === undefined) { return []; @@ -87,31 +88,33 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { const onClickContainer = (group: any, i: number) => SelectNode(selectedNodeKey, group.offset + i, 'logs', appContext); - tabs = tabs.concat([ - { - key: 'logs', - icon: 'fa fa-align-left', - title: 'LOGS', - content: ( -
- appContext.navigation.goto('.', {page: pageData.number, untilTimes: pageData.untilTimes.join(',')})} - containerGroups={containerGroups} - onClickContainer={onClickContainer} - /> -
- ) - } - ]); - if (execEnabled) { + if (logsAllowed) { + tabs = tabs.concat([ + { + key: 'logs', + icon: 'fa fa-align-left', + title: 'LOGS', + content: ( +
+ appContext.navigation.goto('.', {page: pageData.number, untilTimes: pageData.untilTimes.join(',')})} + containerGroups={containerGroups} + onClickContainer={onClickContainer} + /> +
+ ) + } + ]); + } + if (execEnabled && execAllowed) { tabs = tabs.concat([ { key: 'exec', @@ -124,15 +127,18 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { ]); } } - if (ExtensionComponent && state) { - tabs.push({ - title: 'More', - key: 'extension', - content: ( - - - - ) + if (state) { + extensionTabs.forEach((tabExtensions, i) => { + tabs.push({ + title: tabExtensions.title, + key: `extension-${i}`, + content: ( + + + + ), + icon: tabExtensions.icon + }); }); } return tabs; @@ -209,19 +215,17 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { content: }); - return tabs; + const extensionTabs = services.extensions.getResourceTabs('argoproj.io', 'Application').map((ext, i) => ({ + title: ext.title, + key: `extension-${i}`, + content: , + icon: ext.icon + })); + + return tabs.concat(extensionTabs); }; - const [extension, , error] = useData( - async () => { - if (selectedNode?.kind && selectedNode?.group) { - return await services.extensions.loadResourceExtension(selectedNode?.group || '', selectedNode?.kind || ''); - } - }, - null, - null, - [selectedNode] - ); + const extensions = selectedNode?.kind ? services.extensions.getResourceTabs(selectedNode?.group || '', selectedNode?.kind) : []; return (
@@ -266,8 +270,9 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { const settings = await services.authService.settings(); const execEnabled = settings.execEnabled; - - return {controlledState, liveState, events, podState, execEnabled}; + const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name); + const execAllowed = await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name); + return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed}; }}> {data => ( @@ -302,7 +307,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { data.liveState, data.podState, data.events, - error.state ? null : extension?.component, + extensions, [ { title: 'SUMMARY', @@ -311,7 +316,9 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { content: } ], - data.execEnabled + data.execEnabled, + data.execAllowed, + data.logsAllowed )} selectedTabKey={props.tab} onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})} diff --git a/ui/src/app/applications/components/utils.tsx b/ui/src/app/applications/components/utils.tsx index 49b868f4ccfb6..059df1ace741f 100644 --- a/ui/src/app/applications/components/utils.tsx +++ b/ui/src/app/applications/components/utils.tsx @@ -5,8 +5,8 @@ import * as React from 'react'; import * as ReactForm from 'react-form'; import {Text} from 'react-form'; import * as moment from 'moment'; -import {BehaviorSubject, from, fromEvent, merge, Observable, Observer, Subscription} from 'rxjs'; -import {debounceTime} from 'rxjs/operators'; +import {BehaviorSubject, combineLatest, concat, from, fromEvent, Observable, Observer, Subscription} from 'rxjs'; +import {debounceTime, map} from 'rxjs/operators'; import {AppContext, Context, ContextApis} from '../../shared/context'; import {ResourceTreeNode} from './application-resource-tree/application-resource-tree'; @@ -321,7 +321,6 @@ function getActionItems( appChanged: BehaviorSubject, isQuickStart: boolean ): Observable { - let menuItems: Observable; const isRoot = resource.root && nodeKey(resource.root) === nodeKey(resource); const items: MenuItem[] = [ ...((isRoot && [ @@ -355,42 +354,61 @@ function getActionItems( action: () => appContext.apis.navigation.goto('.', {node: nodeKey(resource), tab: 'logs'}, {replace: true}) }); } - if (resource.kind === 'Pod') { - items.push({ - title: 'Exec', - iconClassName: 'fa fa-terminal', - action: () => appContext.apis.navigation.goto('.', {node: nodeKey(resource), tab: 'exec'}, {replace: true}) - }); - } + if (isQuickStart) { return from([items]); } + + const execAction = services.authService + .settings() + .then(async settings => { + const execAllowed = await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name); + if (resource.kind === 'Pod' && settings.execEnabled && execAllowed) { + return [ + { + title: 'Exec', + iconClassName: 'fa fa-terminal', + action: async () => appContext.apis.navigation.goto('.', {node: nodeKey(resource), tab: 'exec'}, {replace: true}) + } as MenuItem + ]; + } + return [] as MenuItem[]; + }) + .catch(() => [] as MenuItem[]); + const resourceActions = services.applications .getResourceActions(application.metadata.name, resource) .then(actions => { - return items.concat( - actions.map(action => ({ - title: action.name, - disabled: !!action.disabled, - action: async () => { - try { - const confirmed = await appContext.apis.popup.confirm(`Execute '${action.name}' action?`, `Are you sure you want to execute '${action.name}' action?`); - if (confirmed) { - await services.applications.runResourceAction(application.metadata.name, resource, action.name); + return actions.map( + action => + ({ + title: action.name, + disabled: !!action.disabled, + action: async () => { + try { + const confirmed = await appContext.apis.popup.confirm( + `Execute '${action.name}' action?`, + `Are you sure you want to execute '${action.name}' action?` + ); + if (confirmed) { + await services.applications.runResourceAction(application.metadata.name, resource, action.name); + } + } catch (e) { + appContext.apis.notifications.show({ + content: , + type: NotificationType.Error + }); } - } catch (e) { - appContext.apis.notifications.show({ - content: , - type: NotificationType.Error - }); } - } - })) + } as MenuItem) ); }) - .catch(() => items); - menuItems = merge(from([items]), from(resourceActions)); - return menuItems; + .catch(() => [] as MenuItem[]); + return combineLatest( + from([items]), // this resolves immediately + concat([[] as MenuItem[]], resourceActions), // this resolves at first to [] and then whatever the API returns + concat([[] as MenuItem[]], execAction) // this resolves at first to [] and then whatever the API returns + ).pipe(map(res => ([] as MenuItem[]).concat(...res))); } export function renderResourceMenu( diff --git a/ui/src/app/help/components/help.tsx b/ui/src/app/help/components/help.tsx index bf9b49a21554d..d27427c089e67 100644 --- a/ui/src/app/help/components/help.tsx +++ b/ui/src/app/help/components/help.tsx @@ -38,12 +38,12 @@ export const Help = () => {

Want to download the CLI tool?

- Linux (amd64) + Linux ({process.env.HOST_ARCH})   {Object.keys(binaryUrls || {}).map(binaryName => { const url = binaryUrls[binaryName]; - const match = binaryName.match(/.*(darwin|windows|linux)-(amd64|arm64)/); + const match = binaryName.match(/.*(darwin|windows|linux)-(amd64|arm64|ppc64le|s390x)/); const [platform, arch] = match ? match.slice(1) : ['', '']; return ( <> diff --git a/ui/src/app/index.html b/ui/src/app/index.html index 5b77a9d56971b..540c5d7a3a21b 100644 --- a/ui/src/app/index.html +++ b/ui/src/app/index.html @@ -9,6 +9,7 @@ + @@ -20,5 +21,5 @@
- + diff --git a/ui/src/app/shared/services/accounts-service.ts b/ui/src/app/shared/services/accounts-service.ts index 8820fa46dd697..008505a63714a 100644 --- a/ui/src/app/shared/services/accounts-service.ts +++ b/ui/src/app/shared/services/accounts-service.ts @@ -27,4 +27,8 @@ export class AccountsService { public deleteToken(name: string, id: string): Promise { return requests.delete(`/account/${name}/token/${id}`); } + + public canI(resource: string, action: string, subresource: string): Promise { + return requests.get(`/account/can-i/${resource}/${action}/${subresource}`).then(res => res.body.value === 'yes'); + } } diff --git a/ui/src/app/shared/services/extensions-service.ts b/ui/src/app/shared/services/extensions-service.ts index 776f6f15a8456..60d9410f3bf70 100644 --- a/ui/src/app/shared/services/extensions-service.ts +++ b/ui/src/app/shared/services/extensions-service.ts @@ -1,40 +1,62 @@ import * as React from 'react'; -import {ApplicationTree, State} from '../models'; +import * as minimatch from 'minimatch'; -const extensions: {resources: {[key: string]: Extension}} = {resources: {}}; -const cache = new Map>(); +import {Application, ApplicationTree, State} from '../models'; + +const extensions = { + resourceExtentions: new Array() +}; + +function registerResourceExtension(component: ExtensionComponent, group: string, kind: string, tabTitle: string, opts?: {icon: string}) { + extensions.resourceExtentions.push({component, group, kind, title: tabTitle, icon: opts?.icon}); +} + +let legacyInitialized = false; + +function initLegacyExtensions() { + if (legacyInitialized) { + return; + } + legacyInitialized = true; + const resources = (window as any).extensions.resources; + Object.keys(resources).forEach(key => { + const [group, kind] = key.split('/'); + registerResourceExtension(resources[key].component, group, kind, 'More'); + }); +} + +export interface ResourceTabExtension { + title: string; + group: string; + kind: string; + component: ExtensionComponent; + icon?: string; +} + +export type ExtensionComponent = React.ComponentType; export interface Extension { - component: React.ComponentType; + component: ExtensionComponent; } export interface ExtensionComponentProps { resource: State; tree: ApplicationTree; + application: Application; } export class ExtensionsService { - public async loadResourceExtension(group: string, kind: string): Promise { - const key = `${group}/${kind}`; - const res = - cache.get(key) || - new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = `extensions/resources/${group}/${kind}/ui/extensions.js`; - script.onload = () => { - const ext = extensions.resources[key]; - if (!ext) { - reject(`Failed to load extension for ${group}/${kind}`); - } else { - resolve(ext); - } - }; - script.onerror = reject; - document.body.appendChild(script); - }); - cache.set(key, res); - return res; + public getResourceTabs(group: string, kind: string): ResourceTabExtension[] { + initLegacyExtensions(); + const items = extensions.resourceExtentions.filter(extension => minimatch(group, extension.group) && minimatch(kind, extension.kind)).slice(); + return items.sort((a, b) => a.title.localeCompare(b.title)); } } -(window as any).extensions = extensions; +((window: any) => { + // deprecated: kept for backwards compatibility + window.extensions = {resources: {}}; + window.extensionsAPI = { + registerResourceExtension + }; +})(window); diff --git a/ui/yarn.lock b/ui/yarn.lock index a945b2ea337c9..31ad6c702899b 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -6328,9 +6328,9 @@ moment-timezone@^0.5.33: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.29.2, moment@^2.29.4: +"moment@>= 2.9.0", moment@^2.24.0, moment@^2.25.3, moment@^2.29.2, moment@^2.29.4: version "2.29.4" - resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== monaco-editor-webpack-plugin@^7.0.0: @@ -6777,10 +6777,10 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-path@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz#4bf424e6b743fb080831f03b536af9fc43f0ffea" - integrity "sha1-+TMVLzxtNPTPNs/D0HsTisETZJ0= sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==" +parse-path@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.4.tgz#4bf424e6b743fb080831f03b536af9fc43f0ffea" + integrity sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw== dependencies: is-ssh "^1.3.0" protocols "^1.4.0" @@ -6788,9 +6788,9 @@ parse-path@^5.0.0: query-string "^6.13.8" parse-url@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/parse-url/-/parse-url-6.0.2.tgz#4a30b057bfc452af64512dfb1a7755c103db3ea1" - integrity sha512-uCSjOvD3T+6B/sPWhR+QowAZcU/o4bjPrVBQBGFxcDF6J6FraCGIaDBsdoQawiaaAVdHvtqBe3w3vKlfBKySOQ== + version "6.0.5" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.5.tgz#4acab8982cef1846a0f8675fa686cef24b2f6f9b" + integrity sha512-e35AeLTSIlkw/5GFq70IN7po8fmDUjpDPY1rIK+VubRfsUvBonjQ+PBZG+vWMACnQSmNlvl524IucoDmcioMxA== dependencies: is-ssh "^1.3.0" normalize-url "^6.1.0" diff --git a/util/argo/resource_tracking.go b/util/argo/resource_tracking.go index 9c8f4a75d8f80..1741bf413d05d 100644 --- a/util/argo/resource_tracking.go +++ b/util/argo/resource_tracking.go @@ -29,6 +29,7 @@ var LabelMaxLength = 63 // ResourceTracking defines methods which allow setup and retrieve tracking information to resource type ResourceTracking interface { GetAppName(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod) string + GetAppInstance(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod) *AppInstanceValue SetAppInstance(un *unstructured.Unstructured, key, val, namespace string, trackingMethod v1alpha1.TrackingMethod) error BuildAppInstanceValue(value AppInstanceValue) string ParseAppInstanceValue(value string) (*AppInstanceValue, error) @@ -54,7 +55,7 @@ func NewResourceTracking() ResourceTracking { // GetTrackingMethod retrieve tracking method from settings func GetTrackingMethod(settingsMgr *settings.SettingsManager) v1alpha1.TrackingMethod { tm, err := settingsMgr.GetTrackingMethod() - if err != nil { + if err != nil || tm == "" { return TrackingMethodLabel } return v1alpha1.TrackingMethod(tm) @@ -64,15 +65,23 @@ func IsOldTrackingMethod(trackingMethod string) bool { return trackingMethod == "" || trackingMethod == string(TrackingMethodLabel) } +func (rt *resourceTracking) getAppInstanceValue(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod) *AppInstanceValue { + appInstanceAnnotation := argokube.GetAppInstanceAnnotation(un, common.AnnotationKeyAppInstance) + value, err := rt.ParseAppInstanceValue(appInstanceAnnotation) + if err != nil { + return nil + } + return value +} + // GetAppName retrieve application name base on tracking method func (rt *resourceTracking) GetAppName(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod) string { retrieveAppInstanceValue := func() string { - appInstanceAnnotation := argokube.GetAppInstanceAnnotation(un, common.AnnotationKeyAppInstance) - value, err := rt.ParseAppInstanceValue(appInstanceAnnotation) - if err != nil { - return "" + value := rt.getAppInstanceValue(un, key, trackingMethod) + if value != nil { + return value.ApplicationName } - return value.ApplicationName + return "" } switch trackingMethod { case TrackingMethodLabel: @@ -86,6 +95,18 @@ func (rt *resourceTracking) GetAppName(un *unstructured.Unstructured, key string } } +// GetAppInstance returns the representation of the app instance annotation. +// If the tracking method does not support metadata, or the annotation could +// not be parsed, it returns nil. +func (rt *resourceTracking) GetAppInstance(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod) *AppInstanceValue { + switch trackingMethod { + case TrackingMethodAnnotation, TrackingMethodAnnotationAndLabel: + return rt.getAppInstanceValue(un, key, trackingMethod) + default: + return nil + } +} + // SetAppInstance set label/annotation base on tracking method func (rt *resourceTracking) SetAppInstance(un *unstructured.Unstructured, key, val, namespace string, trackingMethod v1alpha1.TrackingMethod) error { setAppInstanceAnnotation := func() error { diff --git a/util/git/client.go b/util/git/client.go index fd7eb9fa770ae..c6137bc3dd8be 100644 --- a/util/git/client.go +++ b/util/git/client.go @@ -58,6 +58,7 @@ type Client interface { Root() string Init() error Fetch(revision string) error + Submodule() error Checkout(revision string, submoduleEnabled bool) error LsRefs() (*Refs, error) LsRemote(revision string) (string, error) @@ -392,6 +393,17 @@ func (m *nativeGitClient) LsLargeFiles() ([]string, error) { return ss, nil } +// Submodule embed other repositories into this repository +func (m *nativeGitClient) Submodule() error { + if err := m.runCredentialedCmd("git", "submodule", "sync", "--recursive"); err != nil { + return err + } + if err := m.runCredentialedCmd("git", "submodule", "update", "--init", "--recursive"); err != nil { + return err + } + return nil +} + // Checkout checkout specified revision func (m *nativeGitClient) Checkout(revision string, submoduleEnabled bool) error { if revision == "" || revision == "HEAD" { @@ -415,7 +427,7 @@ func (m *nativeGitClient) Checkout(revision string, submoduleEnabled bool) error } if _, err := os.Stat(m.root + "/.gitmodules"); !os.IsNotExist(err) { if submoduleEnabled { - if err := m.runCredentialedCmd("git", "submodule", "update", "--init", "--recursive"); err != nil { + if err := m.Submodule(); err != nil { return err } } diff --git a/util/git/client_test.go b/util/git/client_test.go index 4addc82aa4413..ce3f05cb0d0d5 100644 --- a/util/git/client_test.go +++ b/util/git/client_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -92,3 +93,117 @@ func Test_nativeGitClient_Fetch_Prune(t *testing.T) { err = client.Fetch("") assert.NoError(t, err) } + +func Test_nativeGitClient_Submodule(t *testing.T) { + tempDir, err := os.MkdirTemp("", "") + require.NoError(t, err) + + foo := filepath.Join(tempDir, "foo") + err = os.Mkdir(foo, 0755) + require.NoError(t, err) + + cmd := exec.Command("git", "init") + cmd.Dir = foo + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + bar := filepath.Join(tempDir, "bar") + err = os.Mkdir(bar, 0755) + require.NoError(t, err) + + cmd = exec.Command("git", "init") + cmd.Dir = bar + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty") + cmd.Dir = bar + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + // Embed repository bar into repository foo + cmd = exec.Command("git", "submodule", "add", bar) + cmd.Dir = foo + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + cmd = exec.Command("git", "commit", "-m", "Initial commit") + cmd.Dir = foo + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + tempDir, err = os.MkdirTemp("", "") + require.NoError(t, err) + + // Clone foo + cmd = exec.Command("git", "clone", foo) + cmd.Dir = tempDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + client, err := NewClient(fmt.Sprintf("file://%s", foo), NopCreds{}, true, false, "") + require.NoError(t, err) + + err = client.Init() + require.NoError(t, err) + + err = client.Fetch("") + assert.NoError(t, err) + + commitSHA, err := client.LsRemote("HEAD") + assert.NoError(t, err) + + // Call Checkout() with submoduleEnabled=false. + err = client.Checkout(commitSHA, false) + assert.NoError(t, err) + + // Check if submodule url does not exist in .git/config + cmd = exec.Command("git", "config", "submodule.bar.url") + cmd.Dir = client.Root() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + assert.Error(t, err) + + // Call Submodule() via Checkout() with submoduleEnabled=true. + err = client.Checkout(commitSHA, true) + assert.NoError(t, err) + + // Check if the .gitmodule URL is reflected in .git/config + cmd = exec.Command("git", "config", "submodule.bar.url") + cmd.Dir = client.Root() + result, err := cmd.Output() + assert.NoError(t, err) + assert.Equal(t, bar+"\n", string(result)) + + // Change URL of submodule bar + cmd = exec.Command("git", "config", "--file=.gitmodules", "submodule.bar.url", bar+"baz") + cmd.Dir = client.Root() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + require.NoError(t, err) + + // Call Submodule() + err = client.Submodule() + assert.NoError(t, err) + + // Check if the URL change in .gitmodule is reflected in .git/config + cmd = exec.Command("git", "config", "submodule.bar.url") + cmd.Dir = client.Root() + result, err = cmd.Output() + assert.NoError(t, err) + assert.Equal(t, bar+"baz\n", string(result)) +} diff --git a/util/git/mocks/Client.go b/util/git/mocks/Client.go index c1624df0617a8..3d4471b1f23c1 100644 --- a/util/git/mocks/Client.go +++ b/util/git/mocks/Client.go @@ -3,9 +3,8 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" - git "github.com/argoproj/argo-cd/v2/util/git" + mock "github.com/stretchr/testify/mock" ) // Client is an autogenerated mock type for the Client type @@ -203,6 +202,20 @@ func (_m *Client) Root() string { return r0 } +// Submodule provides a mock function with given fields: +func (_m *Client) Submodule() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + // VerifyCommitSignature provides a mock function with given fields: _a0 func (_m *Client) VerifyCommitSignature(_a0 string) (string, error) { ret := _m.Called(_a0) diff --git a/util/grpc/grpc.go b/util/grpc/grpc.go index 883dd31f36100..b2c9452c3c27a 100644 --- a/util/grpc/grpc.go +++ b/util/grpc/grpc.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/argoproj/argo-cd/v2/common" "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/grpc" @@ -87,7 +88,7 @@ func BlockingDial(ctx context.Context, network, address string, creds credential grpc.FailOnNonTempDialError(true), grpc.WithContextDialer(dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), // we are handling TLS, so tell grpc not to - grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: 10 * time.Second}), + grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: common.GRPCKeepAliveTime}), ) conn, err := grpc.DialContext(ctx, address, opts...) var res interface{} @@ -115,7 +116,7 @@ type TLSTestResult struct { InsecureErr error } -func TestTLS(address string) (*TLSTestResult, error) { +func TestTLS(address string, dialTime time.Duration) (*TLSTestResult, error) { if parts := strings.Split(address, ":"); len(parts) == 1 { // If port is unspecified, assume the most likely port address += ":443" @@ -124,12 +125,21 @@ func TestTLS(address string) (*TLSTestResult, error) { var tlsConfig tls.Config tlsConfig.InsecureSkipVerify = true creds := credentials.NewTLS(&tlsConfig) - conn, err := BlockingDial(context.Background(), "tcp", address, creds) + + // Set timeout when dialing to the server + // fix: https://github.com/argoproj/argo-cd/issues/9679 + ctx, cancel := context.WithTimeout(context.Background(), dialTime) + defer cancel() + + conn, err := BlockingDial(ctx, "tcp", address, creds) if err == nil { _ = conn.Close() testResult.TLS = true - creds := credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}) - conn, err := BlockingDial(context.Background(), "tcp", address, creds) + creds := credentials.NewTLS(&tls.Config{}) + ctx, cancel := context.WithTimeout(context.Background(), dialTime) + defer cancel() + + conn, err := BlockingDial(ctx, "tcp", address, creds) if err == nil { _ = conn.Close() } else { @@ -142,7 +152,9 @@ func TestTLS(address string) (*TLSTestResult, error) { // If we get here, we were unable to connect via TLS (even with InsecureSkipVerify: true) // It may be because server is running without TLS, or because of real issues (e.g. connection // refused). Test if server accepts plain-text connections - conn, err = BlockingDial(context.Background(), "tcp", address, nil) + ctx, cancel = context.WithTimeout(context.Background(), dialTime) + defer cancel() + conn, err = BlockingDial(ctx, "tcp", address, nil) if err == nil { _ = conn.Close() testResult.TLS = false diff --git a/util/ksonnet/ksonnet.go b/util/ksonnet/ksonnet.go deleted file mode 100644 index ace34cf5d5670..0000000000000 --- a/util/ksonnet/ksonnet.go +++ /dev/null @@ -1,173 +0,0 @@ -package ksonnet - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/argoproj/gitops-engine/pkg/utils/kube" - "github.com/ghodss/yaml" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" - executil "github.com/argoproj/argo-cd/v2/util/exec" -) - -// Destination returns the deployment destination for an environment in app spec data -func Destination(data []byte, environment string) (*v1alpha1.ApplicationDestination, error) { - var appSpec struct { - Environments map[string]struct { - Destination v1alpha1.ApplicationDestination - } - } - err := yaml.Unmarshal(data, &appSpec) - if err != nil { - return nil, fmt.Errorf("could not unmarshal ksonnet spec app.yaml: %v", err) - } - - envSpec, ok := appSpec.Environments[environment] - if !ok { - return nil, fmt.Errorf("environment '%s' does not exist in ksonnet app", environment) - } - - return &envSpec.Destination, nil -} - -// KsonnetApp represents a ksonnet application directory and provides wrapper functionality around -// the `ks` command. -type KsonnetApp interface { - // Root is the root path ksonnet application directory - Root() string - - // Show returns a list of unstructured objects that would be applied to an environment - Show(environment string) ([]*unstructured.Unstructured, error) - - // Destination returns the deployment destination for an environment - Destination(environment string) (*v1alpha1.ApplicationDestination, error) - - // ListParams returns list of ksonnet parameters - ListParams(environment string) ([]*v1alpha1.KsonnetParameter, error) - - // SetComponentParams updates component parameter in specified environment. - SetComponentParams(environment string, component string, param string, value string) error -} - -// Version returns the version of ksonnet used when running ksonnet commands -func Version() (string, error) { - ksApp := ksonnetApp{} - out, err := ksApp.ksCmd("", "version") - if err != nil { - return "", fmt.Errorf("unable to determine ksonnet version: %v", err) - } - ksonnetVersionStr := strings.Split(out, "\n")[0] - parts := strings.SplitN(ksonnetVersionStr, ":", 2) - if len(parts) != 2 { - return "", fmt.Errorf("unexpected version string format: %s", ksonnetVersionStr) - } - version := strings.TrimSpace(parts[1]) - if version[0] != 'v' { - version = "v" + version - } - return version, nil -} - -type ksonnetApp struct { - rootDir string -} - -var _ KsonnetApp = &ksonnetApp{} - -// NewKsonnetApp tries to create a new wrapper to run commands on the `ks` command-line tool. -func NewKsonnetApp(path string) (KsonnetApp, error) { - ksApp := ksonnetApp{rootDir: path} - // ensure that the file exists - if _, err := ksApp.appYamlPath(); err != nil { - return nil, err - } - return &ksApp, nil -} - -func (k *ksonnetApp) appYamlPath() (string, error) { - const appYamlName = "app.yaml" - p := filepath.Clean(filepath.Join(k.Root(), appYamlName)) - if _, err := os.Stat(p); err != nil { - return "", err - } - return p, nil -} - -func (k *ksonnetApp) ksCmd(args ...string) (string, error) { - cmd := exec.Command("ks", args...) - cmd.Dir = k.Root() - - return executil.Run(cmd) -} - -func (k *ksonnetApp) Root() string { - return k.rootDir -} - -// Show generates a concatenated list of Kubernetes manifests in the given environment. -func (k *ksonnetApp) Show(environment string) ([]*unstructured.Unstructured, error) { - out, err := k.ksCmd("show", environment) - if err != nil { - return nil, fmt.Errorf("`ks show` failed: %v", err) - } - return kube.SplitYAML([]byte(out)) -} - -// Destination returns the deployment destination for an environment -func (k *ksonnetApp) Destination(environment string) (*v1alpha1.ApplicationDestination, error) { - p, err := k.appYamlPath() - if err != nil { - return nil, err - } - data, err := ioutil.ReadFile(filepath.Clean(p)) - if err != nil { - return nil, err - } - return Destination(data, environment) -} - -// ListParams returns list of ksonnet parameters -func (k *ksonnetApp) ListParams(environment string) ([]*v1alpha1.KsonnetParameter, error) { - args := []string{"param", "list", "--output", "json"} - if environment != "" { - args = append(args, "--env", environment) - } - out, err := k.ksCmd(args...) - if err != nil { - return nil, err - } - // Auxiliary data to hold unmarshaled JSON output, which may use different field names - var ksParams struct { - Data []struct { - Component string `json:"component"` - Key string `json:"param"` - Value string `json:"value"` - } `json:"data"` - } - if err := json.Unmarshal([]byte(out), &ksParams); err != nil { - return nil, err - } - var params []*v1alpha1.KsonnetParameter - for _, ksParam := range ksParams.Data { - value := strings.Trim(ksParam.Value, `'"`) - params = append(params, &v1alpha1.KsonnetParameter{ - Component: ksParam.Component, - Name: ksParam.Key, - Value: value, - }) - } - return params, nil -} - -// SetComponentParams updates component parameter in specified environment. -func (k *ksonnetApp) SetComponentParams(environment string, component string, param string, value string) error { - _, err := k.ksCmd("param", "set", component, param, value, "--env", environment) - return err -} diff --git a/util/oidc/oidc.go b/util/oidc/oidc.go index 20b9a5fef8b8a..deeb1e2ee9310 100644 --- a/util/oidc/oidc.go +++ b/util/oidc/oidc.go @@ -28,6 +28,8 @@ import ( "github.com/argoproj/argo-cd/v2/util/settings" ) +var InvalidRedirectURLError = fmt.Errorf("invalid return URL") + const ( GrantTypeAuthorizationCode = "authorization_code" GrantTypeImplicit = "implicit" @@ -185,10 +187,18 @@ func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state return "", err } cookieVal := string(val) - returnURL := a.baseHRef + redirectURL := a.baseHRef parts := strings.SplitN(cookieVal, ":", 2) if len(parts) == 2 && parts[1] != "" { - returnURL = parts[1] + if !isValidRedirectURL(parts[1], []string{a.settings.URL, a.baseHRef}) { + sanitizedUrl := parts[1] + if len(sanitizedUrl) > 100 { + sanitizedUrl = sanitizedUrl[:100] + } + log.Warnf("Failed to verify app state - got invalid redirectURL %q", sanitizedUrl) + return "", fmt.Errorf("failed to verify app state: %w", InvalidRedirectURLError) + } + redirectURL = parts[1] } if parts[0] != state { return "", fmt.Errorf("invalid state in '%s' cookie", common.AuthCookieName) @@ -201,7 +211,7 @@ func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state SameSite: http.SameSiteLaxMode, Secure: a.secureCookie, }) - return returnURL, nil + return redirectURL, nil } // isValidRedirectURL checks whether the given redirectURL matches on of the diff --git a/util/oidc/oidc_test.go b/util/oidc/oidc_test.go index 5c94a661c4161..2025f36b12e71 100644 --- a/util/oidc/oidc_test.go +++ b/util/oidc/oidc_test.go @@ -1,12 +1,15 @@ package oidc import ( + "crypto/tls" "encoding/hex" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" + "strings" "testing" gooidc "github.com/coreos/go-oidc" @@ -19,6 +22,7 @@ import ( "github.com/argoproj/argo-cd/v2/util" "github.com/argoproj/argo-cd/v2/util/crypto" "github.com/argoproj/argo-cd/v2/util/settings" + "github.com/argoproj/argo-cd/v2/util/test" ) func TestInferGrantType(t *testing.T) { @@ -104,6 +108,220 @@ func TestHandleCallback(t *testing.T) { assert.Equal(t, "login-failed: <script>alert('hello')</script>\n", w.Body.String()) } +func TestClientApp_HandleLogin(t *testing.T) { + oidcTestServer := test.GetOIDCTestServer(t) + t.Cleanup(oidcTestServer.Close) + + dexTestServer := test.GetDexTestServer(t) + t.Cleanup(dexTestServer.Close) + + t.Run("oidc certificate checking during login should toggle on config", func(t *testing.T) { + cdSettings := &settings.ArgoCDSettings{ + URL: "https://argocd.example.com", + OIDCConfigRAW: fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`, oidcTestServer.URL), + } + app, err := NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + req := httptest.NewRequest("GET", "https://argocd.example.com/auth/login", nil) + + w := httptest.NewRecorder() + + app.HandleLogin(w, req) + + if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + + cdSettings.OIDCTLSInsecureSkipVerify = true + + app, err = NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + w = httptest.NewRecorder() + + app.HandleLogin(w, req) + + assert.NotContains(t, w.Body.String(), "certificate is not trusted") + assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") + }) + + t.Run("dex certificate checking during login should toggle on config", func(t *testing.T) { + cdSettings := &settings.ArgoCDSettings{ + URL: "https://argocd.example.com", + DexConfig: `connectors: +- type: github + name: GitHub + config: + clientID: aabbccddeeff00112233 + clientSecret: aabbccddeeff00112233`, + } + cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey) + require.NoError(t, err) + cdSettings.Certificate = &cert + + app, err := NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + req := httptest.NewRequest("GET", "https://argocd.example.com/auth/login", nil) + + w := httptest.NewRecorder() + + app.HandleLogin(w, req) + + if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + + cdSettings.OIDCTLSInsecureSkipVerify = true + + app, err = NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + w = httptest.NewRecorder() + + app.HandleLogin(w, req) + + assert.NotContains(t, w.Body.String(), "certificate is not trusted") + assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") + }) +} + +func Test_Login_Flow(t *testing.T) { + // Show that SSO login works when no redirect URL is provided, and we fall back to the configured base href for the + // Argo CD instance. + + oidcTestServer := test.GetOIDCTestServer(t) + t.Cleanup(oidcTestServer.Close) + + cdSettings := &settings.ArgoCDSettings{ + URL: "https://argocd.example.com", + OIDCConfigRAW: fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`, oidcTestServer.URL), + OIDCTLSInsecureSkipVerify: true, + } + + // The base href (the last argument for NewClientApp) is what HandleLogin will fall back to when no explicit + // redirect URL is given. + app, err := NewClientApp(cdSettings, "", "/") + require.NoError(t, err) + + w := httptest.NewRecorder() + + req := httptest.NewRequest("GET", "https://argocd.example.com/auth/login", nil) + + app.HandleLogin(w, req) + + redirectUrl, err := w.Result().Location() + require.NoError(t, err) + + state := redirectUrl.Query()["state"] + + req = httptest.NewRequest("GET", fmt.Sprintf("https://argocd.example.com/auth/callback?state=%s&code=abc", state), nil) + for _, cookie := range w.Result().Cookies() { + req.AddCookie(cookie) + } + + w = httptest.NewRecorder() + + app.HandleCallback(w, req) + + assert.NotContains(t, w.Body.String(), InvalidRedirectURLError.Error()) +} + +func TestClientApp_HandleCallback(t *testing.T) { + oidcTestServer := test.GetOIDCTestServer(t) + t.Cleanup(oidcTestServer.Close) + + dexTestServer := test.GetDexTestServer(t) + t.Cleanup(dexTestServer.Close) + + t.Run("oidc certificate checking during oidc callback should toggle on config", func(t *testing.T) { + cdSettings := &settings.ArgoCDSettings{ + URL: "https://argocd.example.com", + OIDCConfigRAW: fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`, oidcTestServer.URL), + } + app, err := NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + req := httptest.NewRequest("GET", "https://argocd.example.com/auth/callback", nil) + + w := httptest.NewRecorder() + + app.HandleCallback(w, req) + + if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + + cdSettings.OIDCTLSInsecureSkipVerify = true + + app, err = NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + w = httptest.NewRecorder() + + app.HandleCallback(w, req) + + assert.NotContains(t, w.Body.String(), "certificate is not trusted") + assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") + }) + + t.Run("dex certificate checking during oidc callback should toggle on config", func(t *testing.T) { + cdSettings := &settings.ArgoCDSettings{ + URL: "https://argocd.example.com", + DexConfig: `connectors: +- type: github + name: GitHub + config: + clientID: aabbccddeeff00112233 + clientSecret: aabbccddeeff00112233`, + } + cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey) + require.NoError(t, err) + cdSettings.Certificate = &cert + + app, err := NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + req := httptest.NewRequest("GET", "https://argocd.example.com/auth/callback", nil) + + w := httptest.NewRecorder() + + app.HandleCallback(w, req) + + if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + + cdSettings.OIDCTLSInsecureSkipVerify = true + + app, err = NewClientApp(cdSettings, dexTestServer.URL, "https://argocd.example.com") + require.NoError(t, err) + + w = httptest.NewRecorder() + + app.HandleCallback(w, req) + + assert.NotContains(t, w.Body.String(), "certificate is not trusted") + assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") + }) +} + func TestIsValidRedirect(t *testing.T) { var tests = []struct { name string @@ -191,7 +409,7 @@ func TestGenerateAppState(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err) expectedReturnURL := "http://argocd.example.com/" - app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature}, "", "") + app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", "") require.NoError(t, err) generateResponse := httptest.NewRecorder() state, err := app.generateAppState(expectedReturnURL, generateResponse) @@ -219,6 +437,56 @@ func TestGenerateAppState(t *testing.T) { }) } +func TestGenerateAppState_XSS(t *testing.T) { + signature, err := util.MakeSignature(32) + require.NoError(t, err) + app, err := NewClientApp( + &settings.ArgoCDSettings{ + // Only return URLs starting with this base should be allowed. + URL: "https://argocd.example.com", + ServerSignature: signature, + }, + "", "", + ) + require.NoError(t, err) + + t.Run("XSS fails", func(t *testing.T) { + // This attack assumes the attacker has compromised the server's secret key. We use `generateAppState` here for + // convenience, but an attacker with access to the server secret could write their own code to generate the + // malicious cookie. + + expectedReturnURL := "javascript: alert('hi')" + generateResponse := httptest.NewRecorder() + state, err := app.generateAppState(expectedReturnURL, generateResponse) + require.NoError(t, err) + + req := httptest.NewRequest("GET", "/", nil) + for _, cookie := range generateResponse.Result().Cookies() { + req.AddCookie(cookie) + } + + returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) + assert.ErrorIs(t, err, InvalidRedirectURLError) + assert.Empty(t, returnURL) + }) + + t.Run("valid return URL succeeds", func(t *testing.T) { + expectedReturnURL := "https://argocd.example.com/some/path" + generateResponse := httptest.NewRecorder() + state, err := app.generateAppState(expectedReturnURL, generateResponse) + require.NoError(t, err) + + req := httptest.NewRequest("GET", "/", nil) + for _, cookie := range generateResponse.Result().Cookies() { + req.AddCookie(cookie) + } + + returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) + assert.NoError(t, err, InvalidRedirectURLError) + assert.Equal(t, expectedReturnURL, returnURL) + }) +} + func TestGenerateAppState_NoReturnURL(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err) diff --git a/util/session/sessionmanager.go b/util/session/sessionmanager.go index eaa8480368d4b..107fb0d59ed04 100644 --- a/util/session/sessionmanager.go +++ b/util/session/sessionmanager.go @@ -124,10 +124,7 @@ func NewSessionManager(settingsMgr *settings.SettingsManager, projectsLister v1a if err != nil { panic(err) } - tlsConfig := settings.TLSConfig() - if tlsConfig != nil { - tlsConfig.InsecureSkipVerify = true - } + tlsConfig := settings.OIDCTLSConfig() s.client = &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, diff --git a/util/session/sessionmanager_test.go b/util/session/sessionmanager_test.go index 1db2a369bb83b..83327e594ce9c 100644 --- a/util/session/sessionmanager_test.go +++ b/util/session/sessionmanager_test.go @@ -2,11 +2,9 @@ package session import ( "context" + "encoding/pem" "fmt" - "io" "math" - "net/http" - "net/http/httptest" "os" "strconv" "strings" @@ -31,6 +29,7 @@ import ( "github.com/argoproj/argo-cd/v2/util/errors" "github.com/argoproj/argo-cd/v2/util/password" "github.com/argoproj/argo-cd/v2/util/settings" + utiltest "github.com/argoproj/argo-cd/v2/util/test" ) func getProjLister(objects ...runtime.Object) v1alpha1.AppProjectNamespaceLister { @@ -460,46 +459,14 @@ func TestFailedAttemptsExpiry(t *testing.T) { os.Setenv(envLoginFailureWindowSeconds, "") } -func oidcMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - switch r.RequestURI { - case "/.well-known/openid-configuration": - _, err := io.WriteString(w, fmt.Sprintf(` -{ - "issuer": "%[1]s", - "authorization_endpoint": "%[1]s/auth", - "token_endpoint": "%[1]s/token", - "jwks_uri": "%[1]s/keys", - "userinfo_endpoint": "%[1]s/userinfo", - "device_authorization_endpoint": "%[1]s/device/code", - "grant_types_supported": ["authorization_code"], - "response_types_supported": ["code"], - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": ["RS512"], - "code_challenge_methods_supported": ["S256", "plain"], - "scopes_supported": ["openid"], - "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], - "claims_supported": ["sub", "aud", "exp"] -}`, url)) - require.NoError(t, err) - default: - w.WriteHeader(404) - } +func getKubeClientWithConfig(config map[string]string, secretConfig map[string][]byte) *fake.Clientset { + mergedSecretConfig := map[string][]byte{ + "server.secretkey": []byte("Hello, world!"), + } + for key, value := range secretConfig { + mergedSecretConfig[key] = value } -} - -func getOIDCTestServer(t *testing.T) *httptest.Server { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Start with a placeholder. We need the server URL before setting up the real handler. - })) - ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - oidcMockHandler(t, ts.URL)(w, r) - }) - return ts -} -func getKubeClientWithConfig(config map[string]string) *fake.Clientset { return fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "argocd-cm", @@ -514,45 +481,18 @@ func getKubeClientWithConfig(config map[string]string) *fake.Clientset { Name: "argocd-secret", Namespace: "argocd", }, - Data: map[string][]byte{ - "server.secretkey": []byte("Hello, world!"), - }, + Data: mergedSecretConfig, }) } -// privateKey is an RSA key used only for tests. -var privateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEA2KHkp2fe0ReHqAt9BimWEec2ryWZIyg9jvB3BdP3mzFf0bOt -WlHm1FAETFxH4h5jYASUaaWEwRNNyGlT1GhTp+jOMC4xhOSb5/SnI2dt2EITkudQ -FKsSFUdJAndqOzkjrP2pL4fi4b7JWhuLDO36ufAP4l2m3tnAseGSSTIccWvzLFFU -s3wsHOHxOcJGCP1Z7rizxl6mTKYL/Z+GHqN17OJslDf901uPXsUeDCYL2iigGPhD -Ao6k8POsfbpqLG7poCDTK50FLnS5qEocjxt+J4ZjBEWTU/DOFXWYstzfbhm8OZPQ -pSSEiBCxpg+zjtkQfCyZxXB5RQ84CY78fXOI9QIDAQABAoIBAG8jL0FLIp62qZvm -uO9ualUo/37/lP7aaCpq50UQJ9lwjS3yNh8+IWQO4QWj2iUBXg4mi1Vf2ymKk78b -eixgkXp1D0Lcj/8ToYBwnUami04FKDGXhhf0Y8SS27vuM4vKlqjrQd7modkangYi -V0X82UKHDD8fuLpfkGIxzXDLypfMzjMuVpSntnWaf2YX3VR/0/66yEp9GejftF2k -wqhGoWM6r68pN5XuCqWd5PRluSoDy/o4BAFMhYCSfp9PjgZE8aoeWHgYzlZ3gUyn -r+HaDDNWbibhobXk/9h8lwAJ6KCZ5RZ+HFfh0HuwIxmocT9OCFgy/S0g1p+o3m9K -VNd5AMkCgYEA5fbS5UK7FBzuLoLgr1hktmbLJhpt8y8IPHNABHcUdE+O4/1xTQNf -pMUwkKjGG1MtrGjLOIoMGURKKn8lR1GMZueOTSKY0+mAWUGvSzl6vwtJwvJruT8M -otEO03o0tPnRKGxbFjqxkp2b6iqJ8MxCRZ3lSidc4mdi7PHzv9lwgvsCgYEA8Siq -7weCri9N6y+tIdORAXgRzcW54BmJyqB147c72RvbMacb6rN28KXpM3qnRXyp3Llb -yh81TW3FH10GqrjATws7BK8lP9kkAw0Z/7kNiS1NgH3pUbO+5H2kAa/6QW35nzRe -Jw2lyfYGWqYO4hYXH14ML1kjgS1hgd3XHOQ64M8CgYAKcjDYSzS2UC4dnMJaFLjW -dErsGy09a7iDDnUs/r/GHMsP3jZkWi/hCzgOiiwdl6SufUAl/FdaWnjH/2iRGco3 -7nLPXC/3CFdVNp+g2iaSQRADtAFis9N+HeL/hkCYq/RtUqa8lsP0NgacF3yWnKCy -Ct8chDc67ZlXzBHXeCgdOwKBgHHGFPbWXUHeUW1+vbiyvrupsQSanznp8oclMtkv -Dk48hSokw9fzuU6Jh77gw9/Vk7HtxS9Tj+squZA1bDrJFPl1u+9WzkUUJZhG6xgp -bwhj1iejv5rrKUlVOTYOlwudXeJNa4oTNz9UEeVcaLMjZt9GmIsSC90a0uDZD26z -AlAjAoGAEoqm2DcNN7SrH6aVFzj1EVOrNsHYiXj/yefspeiEmf27PSAslP+uF820 -SDpz4h+Bov5qTKkzcxuu1QWtA4M0K8Iy6IYLwb83DZEm1OsAf4i0pODz21PY/I+O -VHzjB10oYgaInHZgMUdyb6F571UdiYSB6a/IlZ3ngj5touy3VIM= ------END RSA PRIVATE KEY-----`) - func TestSessionManager_VerifyToken(t *testing.T) { - t.Run("HS256 is supported", func(t *testing.T) { - oidcTestServer := getOIDCTestServer(t) - defer oidcTestServer.Close() + oidcTestServer := utiltest.GetOIDCTestServer(t) + t.Cleanup(oidcTestServer.Close) + + dexTestServer := utiltest.GetDexTestServer(t) + t.Cleanup(dexTestServer.Close) + + t.Run("RS512 is supported", func(t *testing.T) { dexConfig := map[string]string{ "url": "", "oidc.config": fmt.Sprintf(` @@ -563,7 +503,7 @@ clientSecret: yyy requestedScopes: ["oidc"]`, oidcTestServer.URL), } - settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig), "argocd") + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, nil), "argocd") mgr := NewSessionManager(settingsMgr, getProjLister(), "", NewUserStateStorage(nil)) mgr.verificationDelayNoiseEnabled = false // Use test server's client to avoid TLS issues. @@ -572,7 +512,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} claims.Issuer = oidcTestServer.URL token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) - key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) require.NoError(t, err) tokenString, err := token.SignedString(key) require.NoError(t, err) @@ -580,4 +520,216 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), _, _, err = mgr.VerifyToken(tokenString) assert.NotContains(t, err.Error(), "oidc: id token signed with unsupported algorithm") }) + + t.Run("oidcConfig.rootCA is respected", func(t *testing.T) { + cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: oidcTestServer.TLS.Certificates[0].Certificate[0]}) + + dexConfig := map[string]string{ + "url": "", + "oidc.config": fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"] +rootCA: | + %s +`, oidcTestServer.URL, strings.Replace(string(cert), "\n", "\n ", -1)), + } + + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, nil), "argocd") + mgr := NewSessionManager(settingsMgr, getProjLister(), "", NewUserStateStorage(nil)) + mgr.verificationDelayNoiseEnabled = false + + claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} + claims.Issuer = oidcTestServer.URL + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + + _, _, err = mgr.VerifyToken(tokenString) + // If the root CA is being respected, we won't get this error. The error message is environment-dependent, so + // we check for either of the error messages associated with a failed cert check. + assert.NotContains(t, err.Error(), "certificate is not trusted") + assert.NotContains(t, err.Error(), "certificate signed by unknown authority") + }) + + t.Run("OIDC provider is Dex, TLS is configured", func(t *testing.T) { + dexConfig := map[string]string{ + "url": dexTestServer.URL, + "dex.config": `connectors: +- type: github + name: GitHub + config: + clientID: aabbccddeeff00112233 + clientSecret: aabbccddeeff00112233`, + } + + // This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair + // must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled. + secretConfig := map[string][]byte{ + "tls.crt": utiltest.Cert, + "tls.key": utiltest.PrivateKey, + } + + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd") + mgr := NewSessionManager(settingsMgr, getProjLister(), dexTestServer.URL, NewUserStateStorage(nil)) + mgr.verificationDelayNoiseEnabled = false + + claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} + claims.Issuer = fmt.Sprintf("%s/api/dex", dexTestServer.URL) + token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + + _, _, err = mgr.VerifyToken(tokenString) + require.Error(t, err) + if !strings.Contains(err.Error(), "certificate signed by unknown authority") && !strings.Contains(err.Error(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + }) + + t.Run("OIDC provider is external, TLS is configured", func(t *testing.T) { + dexConfig := map[string]string{ + "url": "", + "oidc.config": fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`, oidcTestServer.URL), + } + + // This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair + // must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled. + secretConfig := map[string][]byte{ + "tls.crt": utiltest.Cert, + "tls.key": utiltest.PrivateKey, + } + + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd") + mgr := NewSessionManager(settingsMgr, getProjLister(), "", NewUserStateStorage(nil)) + mgr.verificationDelayNoiseEnabled = false + + claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} + claims.Issuer = oidcTestServer.URL + token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + + _, _, err = mgr.VerifyToken(tokenString) + require.Error(t, err) + if !strings.Contains(err.Error(), "certificate signed by unknown authority") && !strings.Contains(err.Error(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + }) + + t.Run("OIDC provider is Dex, TLS is configured", func(t *testing.T) { + dexConfig := map[string]string{ + "url": dexTestServer.URL, + "dex.config": `connectors: +- type: github + name: GitHub + config: + clientID: aabbccddeeff00112233 + clientSecret: aabbccddeeff00112233`, + } + + // This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair + // must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled. + secretConfig := map[string][]byte{ + "tls.crt": utiltest.Cert, + "tls.key": utiltest.PrivateKey, + } + + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd") + mgr := NewSessionManager(settingsMgr, getProjLister(), dexTestServer.URL, NewUserStateStorage(nil)) + mgr.verificationDelayNoiseEnabled = false + + claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} + claims.Issuer = fmt.Sprintf("%s/api/dex", dexTestServer.URL) + token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + + _, _, err = mgr.VerifyToken(tokenString) + require.Error(t, err) + if !strings.Contains(err.Error(), "certificate signed by unknown authority") && !strings.Contains(err.Error(), "certificate is not trusted") { + t.Fatal("did not receive expected certificate verification failure error") + } + }) + + t.Run("OIDC provider is external, TLS is configured, OIDCTLSInsecureSkipVerify is true", func(t *testing.T) { + dexConfig := map[string]string{ + "url": "", + "oidc.config": fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`, oidcTestServer.URL), + "oidc.tls.insecure.skip.verify": "true", + } + + // This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair + // must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled. + secretConfig := map[string][]byte{ + "tls.crt": utiltest.Cert, + "tls.key": utiltest.PrivateKey, + } + + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd") + mgr := NewSessionManager(settingsMgr, getProjLister(), "", NewUserStateStorage(nil)) + mgr.verificationDelayNoiseEnabled = false + + claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} + claims.Issuer = oidcTestServer.URL + token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + + _, _, err = mgr.VerifyToken(tokenString) + assert.NotContains(t, err.Error(), "certificate is not trusted") + assert.NotContains(t, err.Error(), "certificate signed by unknown authority") + }) + + t.Run("OIDC provider is external, TLS is not configured, OIDCTLSInsecureSkipVerify is true", func(t *testing.T) { + dexConfig := map[string]string{ + "url": "", + "oidc.config": fmt.Sprintf(` +name: Test +issuer: %s +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`, oidcTestServer.URL), + "oidc.tls.insecure.skip.verify": "true", + } + + settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClientWithConfig(dexConfig, nil), "argocd") + mgr := NewSessionManager(settingsMgr, getProjLister(), "", NewUserStateStorage(nil)) + mgr.verificationDelayNoiseEnabled = false + + claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))} + claims.Issuer = oidcTestServer.URL + token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + + _, _, err = mgr.VerifyToken(tokenString) + // This is the error thrown when the test server's certificate _is_ being verified. + assert.NotContains(t, err.Error(), "certificate is not trusted") + assert.NotContains(t, err.Error(), "certificate signed by unknown authority") + }) } diff --git a/util/settings/settings.go b/util/settings/settings.go index 6eca1c76664c5..e8fd84cbaff15 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -105,6 +105,15 @@ type ArgoCDSettings struct { ServerRBACLogEnforceEnable bool `json:"serverRBACLogEnforceEnable"` // ExecEnabled indicates whether the UI exec feature is enabled ExecEnabled bool `json:"execEnabled"` + // ExecShells restricts which shells are allowed for `exec` and in which order they are tried + ExecShells []string `json:"execShells"` + // OIDCTLSInsecureSkipVerify determines whether certificate verification is skipped when verifying tokens with the + // configured OIDC provider (either external or the bundled Dex instance). Setting this to `true` will cause JWT + // token verification to pass despite the OIDC provider having an invalid certificate. Only set to `true` if you + // understand the risks. + OIDCTLSInsecureSkipVerify bool `json:"oidcTLSInsecureSkipVerify"` + // TrackingMethod defines the resource tracking method to be used + TrackingMethod string `json:"application.resourceTrackingMethod,omitempty"` } type GoogleAnalytics struct { @@ -382,7 +391,7 @@ const ( kustomizePathPrefixKey = "kustomize.path" // anonymousUserEnabledKey is the key which enables or disables anonymous user anonymousUserEnabledKey = "users.anonymous.enabled" - // anonymousUserEnabledKey is the key which specifies token expiration duration + // userSessionDurationKey is the key which specifies token expiration duration userSessionDurationKey = "users.session.duration" // diffOptions is the key where diff options are configured resourceCompareOptionsKey = "resource.compareoptions" @@ -426,6 +435,10 @@ const ( helmValuesFileSchemesKey = "helm.valuesFileSchemes" // execEnabledKey is the key to configure whether the UI exec feature is enabled execEnabledKey = "exec.enabled" + // execShellsKey is the key to configure which shells are allowed for `exec` and in what order they are tried + execShellsKey = "exec.shells" + // oidcTLSInsecureSkipVerifyKey is the key to configure whether TLS cert verification is skipped for OIDC connections + oidcTLSInsecureSkipVerifyKey = "oidc.tls.insecure.skip.verify" ) var ( @@ -1290,7 +1303,7 @@ func (mgr *SettingsManager) ensureSynced(forceResync bool) error { func getDownloadBinaryUrlsFromConfigMap(argoCDCM *apiv1.ConfigMap) map[string]string { binaryUrls := map[string]string{} - for _, archType := range []string{"darwin-amd64", "darwin-arm64", "windows-amd64", "linux-arm64", "linux-amd64"} { + for _, archType := range []string{"darwin-amd64", "darwin-arm64", "windows-amd64", "linux-amd64", "linux-arm64", "linux-ppc64le", "linux-s390x"} { if val, ok := argoCDCM.Data[settingsBinaryUrlsKey+"."+archType]; ok { binaryUrls[archType] = val } @@ -1335,6 +1348,15 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi } settings.InClusterEnabled = argoCDCM.Data[inClusterEnabledKey] != "false" settings.ExecEnabled = argoCDCM.Data[execEnabledKey] == "true" + execShells := argoCDCM.Data[execShellsKey] + if execShells != "" { + settings.ExecShells = strings.Split(execShells, ",") + } else { + // Fall back to default. If you change this list, also change docs/operator-manual/argocd-cm.yaml. + settings.ExecShells = []string{"bash", "sh", "powershell", "cmd"} + } + settings.OIDCTLSInsecureSkipVerify = argoCDCM.Data[oidcTLSInsecureSkipVerifyKey] == "true" + settings.TrackingMethod = argoCDCM.Data[settingsResourceTrackingMethodKey] } // validateExternalURL ensures the external URL that is set on the configmap is valid @@ -1705,23 +1727,28 @@ func (a *ArgoCDSettings) OAuth2ClientSecret() string { return "" } +// OIDCTLSConfig returns the TLS config for the OIDC provider. If an external provider is configured, returns a TLS +// config using the root CAs (if any) specified in the OIDC config. If an external OIDC provider is not configured, +// returns the API server TLS config, because the API server proxies requests to Dex. func (a *ArgoCDSettings) OIDCTLSConfig() *tls.Config { - if oidcConfig := a.OIDCConfig(); oidcConfig != nil { + var tlsConfig *tls.Config + + oidcConfig := a.OIDCConfig() + if oidcConfig != nil { + tlsConfig = &tls.Config{} if oidcConfig.RootCA != "" { certPool := x509.NewCertPool() ok := certPool.AppendCertsFromPEM([]byte(oidcConfig.RootCA)) if !ok { - log.Warn("invalid oidc root ca cert - returning default tls.Config instead") - return &tls.Config{MinVersion: tls.VersionTLS12} - } - return &tls.Config{ - RootCAs: certPool, - MinVersion: tls.VersionTLS12, + log.Warn("failed to append certificates from PEM: proceeding without custom rootCA") + } else { + tlsConfig.RootCAs = certPool } } + } else { + tlsConfig = a.TLSConfig() } - tlsConfig := a.TLSConfig() - if tlsConfig != nil { + if tlsConfig != nil && a.OIDCTLSInsecureSkipVerify { tlsConfig.InsecureSkipVerify = true } return tlsConfig diff --git a/util/settings/settings_test.go b/util/settings/settings_test.go index 9157f3bb4391d..9eecd15099135 100644 --- a/util/settings/settings_test.go +++ b/util/settings/settings_test.go @@ -4,12 +4,15 @@ import ( "context" "crypto/tls" "crypto/x509" + "fmt" "sort" + "strings" "testing" "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" testutil "github.com/argoproj/argo-cd/v2/test" + "github.com/argoproj/argo-cd/v2/util/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -665,11 +668,12 @@ func TestSettingsManager_GetHelp(t *testing.T) { t.Run("GetBinaryUrls", func(t *testing.T) { _, settingsManager := fixtures(map[string]string{ "help.download.darwin-amd64": "amd64-path", + "help.download.linux-s390x": "s390x-path", "help.download.unsupported": "nowhere", }) h, err := settingsManager.GetHelp() assert.NoError(t, err) - assert.Equal(t, map[string]string{"darwin-amd64": "amd64-path"}, h.BinaryURLs) + assert.Equal(t, map[string]string{"darwin-amd64": "amd64-path", "linux-s390x": "s390x-path"}, h.BinaryURLs) }) } @@ -983,6 +987,13 @@ func TestDownloadArgoCDBinaryUrls(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "some-url", argoCDCM.Data["help.download.darwin-amd64"]) + _, settingsManager = fixtures(map[string]string{ + "help.download.linux-s390x": "some-url", + }) + argoCDCM, err = settingsManager.getConfigMap() + assert.NoError(t, err) + assert.Equal(t, "some-url", argoCDCM.Data["help.download.linux-s390x"]) + _, settingsManager = fixtures(map[string]string{ "help.download.unsupported": "some-url", }) @@ -1169,3 +1180,68 @@ func TestGetHelmSettings(t *testing.T) { }) } } +func TestArgoCDSettings_OIDCTLSConfig_OIDCTLSInsecureSkipVerify(t *testing.T) { + certParsed, err := tls.X509KeyPair(test.Cert, test.PrivateKey) + require.NoError(t, err) + + testCases := []struct{ + name string + settings *ArgoCDSettings + expectNilTLSConfig bool + }{ + { + name: "OIDC configured, no root CA", + settings: &ArgoCDSettings{OIDCConfigRAW: `name: Test +issuer: aaa +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"]`}, + }, + { + name: "OIDC configured, valid root CA", + settings: &ArgoCDSettings{OIDCConfigRAW: fmt.Sprintf(` +name: Test +issuer: aaa +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"] +rootCA: | + %s +`, strings.Replace(string(test.Cert), "\n", "\n ", -1))}, + }, + { + name: "OIDC configured, invalid root CA", + settings: &ArgoCDSettings{OIDCConfigRAW: `name: Test +issuer: aaa +clientID: xxx +clientSecret: yyy +requestedScopes: ["oidc"] +rootCA: "invalid"`}, + }, + { + name: "OIDC not configured, no cert configured", + settings: &ArgoCDSettings{}, + expectNilTLSConfig: true, + }, + { + name: "OIDC not configured, cert configured", + settings: &ArgoCDSettings{Certificate: &certParsed}, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + if testCase.expectNilTLSConfig { + assert.Nil(t, testCase.settings.OIDCTLSConfig()) + } else { + assert.False(t, testCase.settings.OIDCTLSConfig().InsecureSkipVerify) + + testCase.settings.OIDCTLSInsecureSkipVerify = true + + assert.True(t, testCase.settings.OIDCTLSConfig().InsecureSkipVerify) + } + }) + } +} diff --git a/util/test/testutil.go b/util/test/testutil.go new file mode 100644 index 0000000000000..3b46bbd1bd296 --- /dev/null +++ b/util/test/testutil.go @@ -0,0 +1,140 @@ +package test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +// Cert is a certificate for tests. It was generated like this: +// opts := tls.CertOptions{Hosts: []string{"localhost"}, Organization: "Acme"} +// certBytes, privKey, err := tls.generatePEM(opts) +var Cert = []byte(`-----BEGIN CERTIFICATE----- +MIIC8zCCAdugAwIBAgIQCSoocl6e/FR4mQy1wX6NbjANBgkqhkiG9w0BAQsFADAP +MQ0wCwYDVQQKEwRBY21lMB4XDTIyMDYyMjE3Mjk1MloXDTIzMDYyMjE3Mjk1Mlow +DzENMAsGA1UEChMEQWNtZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANih5Kdn3tEXh6gLfQYplhHnNq8lmSMoPY7wdwXT95sxX9GzrVpR5tRQBExcR+Ie +Y2AElGmlhMETTchpU9RoU6fozjAuMYTkm+f0pyNnbdhCE5LnUBSrEhVHSQJ3ajs5 +I6z9qS+H4uG+yVobiwzt+rnwD+Jdpt7ZwLHhkkkyHHFr8yxRVLN8LBzh8TnCRgj9 +We64s8ZepkymC/2fhh6jdezibJQ3/dNbj17FHgwmC9oooBj4QwKOpPDzrH26aixu +6aAg0yudBS50uahKHI8bfieGYwRFk1PwzhV1mLLc324ZvDmT0KUkhIgQsaYPs47Z +EHwsmcVweUUPOAmO/H1ziPUCAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud +JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJbG9jYWxo +b3N0MA0GCSqGSIb3DQEBCwUAA4IBAQA+8cGJfYRhXQxan7FATsbtC+1DwW1cPc60 +5eLOuI0jPdvXLDmtOulBEjR4KOfJ5oTKXGjs/+gR3sffP6s8gm2XFQn4+OsmxHbO +b2RjPHgKUtJmrI4ZCN8iPGlKIar5u6Q8NZwzpeZ2XL0bpPp7RQsfHqMyhsqDinWR +vvwQB+Bri0oIOtzW2645vWmYc2SaFMd8+8g6Ipa+PRSJezeUxIVZG12zlhsio18F +9SHY2ONcYISjfrGTIcu4cZRGxCZGTIwMngBlb71mia+K7uH+UE6qfJy/t6KiFsCP +yOwMb95nGQSQLDNoGr8gwgE2qPuR0kR9Z5OrWF0DoVCyL3xnxr02 +-----END CERTIFICATE-----`) + +// PrivateKey is an RSA key used only for tests. +var PrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA2KHkp2fe0ReHqAt9BimWEec2ryWZIyg9jvB3BdP3mzFf0bOt +WlHm1FAETFxH4h5jYASUaaWEwRNNyGlT1GhTp+jOMC4xhOSb5/SnI2dt2EITkudQ +FKsSFUdJAndqOzkjrP2pL4fi4b7JWhuLDO36ufAP4l2m3tnAseGSSTIccWvzLFFU +s3wsHOHxOcJGCP1Z7rizxl6mTKYL/Z+GHqN17OJslDf901uPXsUeDCYL2iigGPhD +Ao6k8POsfbpqLG7poCDTK50FLnS5qEocjxt+J4ZjBEWTU/DOFXWYstzfbhm8OZPQ +pSSEiBCxpg+zjtkQfCyZxXB5RQ84CY78fXOI9QIDAQABAoIBAG8jL0FLIp62qZvm +uO9ualUo/37/lP7aaCpq50UQJ9lwjS3yNh8+IWQO4QWj2iUBXg4mi1Vf2ymKk78b +eixgkXp1D0Lcj/8ToYBwnUami04FKDGXhhf0Y8SS27vuM4vKlqjrQd7modkangYi +V0X82UKHDD8fuLpfkGIxzXDLypfMzjMuVpSntnWaf2YX3VR/0/66yEp9GejftF2k +wqhGoWM6r68pN5XuCqWd5PRluSoDy/o4BAFMhYCSfp9PjgZE8aoeWHgYzlZ3gUyn +r+HaDDNWbibhobXk/9h8lwAJ6KCZ5RZ+HFfh0HuwIxmocT9OCFgy/S0g1p+o3m9K +VNd5AMkCgYEA5fbS5UK7FBzuLoLgr1hktmbLJhpt8y8IPHNABHcUdE+O4/1xTQNf +pMUwkKjGG1MtrGjLOIoMGURKKn8lR1GMZueOTSKY0+mAWUGvSzl6vwtJwvJruT8M +otEO03o0tPnRKGxbFjqxkp2b6iqJ8MxCRZ3lSidc4mdi7PHzv9lwgvsCgYEA8Siq +7weCri9N6y+tIdORAXgRzcW54BmJyqB147c72RvbMacb6rN28KXpM3qnRXyp3Llb +yh81TW3FH10GqrjATws7BK8lP9kkAw0Z/7kNiS1NgH3pUbO+5H2kAa/6QW35nzRe +Jw2lyfYGWqYO4hYXH14ML1kjgS1hgd3XHOQ64M8CgYAKcjDYSzS2UC4dnMJaFLjW +dErsGy09a7iDDnUs/r/GHMsP3jZkWi/hCzgOiiwdl6SufUAl/FdaWnjH/2iRGco3 +7nLPXC/3CFdVNp+g2iaSQRADtAFis9N+HeL/hkCYq/RtUqa8lsP0NgacF3yWnKCy +Ct8chDc67ZlXzBHXeCgdOwKBgHHGFPbWXUHeUW1+vbiyvrupsQSanznp8oclMtkv +Dk48hSokw9fzuU6Jh77gw9/Vk7HtxS9Tj+squZA1bDrJFPl1u+9WzkUUJZhG6xgp +bwhj1iejv5rrKUlVOTYOlwudXeJNa4oTNz9UEeVcaLMjZt9GmIsSC90a0uDZD26z +AlAjAoGAEoqm2DcNN7SrH6aVFzj1EVOrNsHYiXj/yefspeiEmf27PSAslP+uF820 +SDpz4h+Bov5qTKkzcxuu1QWtA4M0K8Iy6IYLwb83DZEm1OsAf4i0pODz21PY/I+O +VHzjB10oYgaInHZgMUdyb6F571UdiYSB6a/IlZ3ngj5touy3VIM= +-----END RSA PRIVATE KEY-----`) + +func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.RequestURI { + case "/api/dex/.well-known/openid-configuration": + _, err := io.WriteString(w, fmt.Sprintf(` +{ + "issuer": "%[1]s/api/dex", + "authorization_endpoint": "%[1]s/api/dex/auth", + "token_endpoint": "%[1]s/api/dex/token", + "jwks_uri": "%[1]s/api/dex/keys", + "userinfo_endpoint": "%[1]s/api/dex/userinfo", + "device_authorization_endpoint": "%[1]s/api/dex/device/code", + "grant_types_supported": ["authorization_code"], + "response_types_supported": ["code"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS512"], + "code_challenge_methods_supported": ["S256", "plain"], + "scopes_supported": ["openid"], + "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], + "claims_supported": ["sub", "aud", "exp"] +}`, url)) + require.NoError(t, err) + default: + w.WriteHeader(404) + } + } +} + +func GetDexTestServer(t *testing.T) *httptest.Server { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Start with a placeholder. We need the server URL before setting up the real handler. + })) + ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dexMockHandler(t, ts.URL)(w, r) + }) + return ts +} + +func oidcMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.RequestURI { + case "/.well-known/openid-configuration": + _, err := io.WriteString(w, fmt.Sprintf(` +{ + "issuer": "%[1]s", + "authorization_endpoint": "%[1]s/auth", + "token_endpoint": "%[1]s/token", + "jwks_uri": "%[1]s/keys", + "userinfo_endpoint": "%[1]s/userinfo", + "device_authorization_endpoint": "%[1]s/device/code", + "grant_types_supported": ["authorization_code"], + "response_types_supported": ["code"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS512"], + "code_challenge_methods_supported": ["S256", "plain"], + "scopes_supported": ["openid"], + "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], + "claims_supported": ["sub", "aud", "exp"] +}`, url)) + require.NoError(t, err) + default: + w.WriteHeader(404) + } + } +} + +func GetOIDCTestServer(t *testing.T) *httptest.Server { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Start with a placeholder. We need the server URL before setting up the real handler. + })) + ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + oidcMockHandler(t, ts.URL)(w, r) + }) + return ts +} \ No newline at end of file diff --git a/util/tls/tls.go b/util/tls/tls.go index d02e0ccd132f8..5bf3477c903d0 100644 --- a/util/tls/tls.go +++ b/util/tls/tls.go @@ -249,6 +249,8 @@ func generate(opts CertOptions) ([]byte, crypto.PrivateKey, error) { var validFor time.Duration if opts.ValidFor == 0 { validFor = 365 * 24 * time.Hour + } else { + validFor = opts.ValidFor } notAfter := notBefore.Add(validFor) diff --git a/util/tls/tls_test.go b/util/tls/tls_test.go index 02ac4319e320c..574c5d6efcb1b 100644 --- a/util/tls/tls_test.go +++ b/util/tls/tls_test.go @@ -252,6 +252,21 @@ func TestGenerate(t *testing.T) { assert.NotNil(t, cert) assert.GreaterOrEqual(t, (time.Now().Unix())+int64(1*time.Hour), cert.NotBefore.Unix()) }) + + for _, year := range []int{1, 2, 3, 10} { + t.Run(fmt.Sprintf("Create certificate with specified ValidFor %d year", year), func(t *testing.T) { + validFrom, validFor := time.Now(), 365*24*time.Hour*time.Duration(year) + opts := CertOptions{Hosts: []string{"localhost"}, Organization: "Acme", ValidFrom: validFrom, ValidFor: validFor} + certBytes, privKey, err := generate(opts) + assert.NoError(t, err) + assert.NotNil(t, privKey) + cert, err := x509.ParseCertificate(certBytes) + assert.NoError(t, err) + assert.NotNil(t, cert) + t.Logf("certificate expiration time %s", cert.NotAfter) + assert.Equal(t, validFrom.Unix()+int64(validFor.Seconds()), cert.NotAfter.Unix()) + }) + } } func TestGeneratePEM(t *testing.T) { diff --git a/util/webhook/webhook.go b/util/webhook/webhook.go index 25f9b799db898..d20406b42586e 100644 --- a/util/webhook/webhook.go +++ b/util/webhook/webhook.go @@ -337,8 +337,13 @@ func appFilesHaveChanged(app *v1alpha1.Application, changedFiles []string) bool f = ensureAbsPath(f) for _, item := range refreshPaths { item = ensureAbsPath(item) - - if _, err := security.EnforceToCurrentRoot(item, f); err == nil { + changed := false + if f == item { + changed = true + } else if _, err := security.EnforceToCurrentRoot(item, f); err == nil { + changed = true + } + if changed { log.WithField("application", app.Name).Debugf("Application uses files that have changed") return true } diff --git a/util/webhook/webhook_test.go b/util/webhook/webhook_test.go index f5161d7e9f561..962b321b478fb 100644 --- a/util/webhook/webhook_test.go +++ b/util/webhook/webhook_test.go @@ -219,6 +219,12 @@ func Test_getAppRefreshPrefix(t *testing.T) { {"absolute path - not matching", getApp("/source/path1", "source/path"), []string{"source/path/my-deployment.yaml"}, false}, {"two relative paths - matching", getApp(".;../shared", "my-app"), []string{"shared/my-deployment.yaml"}, true}, {"two relative paths - not matching", getApp(".;../shared", "my-app"), []string{"README.md"}, false}, + {"file relative path - matching", getApp("./my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, true}, + {"file relative path - not matching", getApp("./my-deployment.yaml", "source/path"), []string{"README.md"}, false}, + {"file absolute path - matching", getApp("/source/path/my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, true}, + {"file absolute path - not matching", getApp("/source/path1/README.md", "source/path"), []string{"source/path/my-deployment.yaml"}, false}, + {"file two relative paths - matching", getApp("./README.md;../shared/my-deployment.yaml", "my-app"), []string{"shared/my-deployment.yaml"}, true}, + {"file two relative paths - not matching", getApp(".README.md;../shared/my-deployment.yaml", "my-app"), []string{"kustomization.yaml"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {