mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-26 20:48:46 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eba5be864 | ||
|
|
cc56c9e8a2 | ||
|
|
30f68e1041 | ||
|
|
e63273e4c1 | ||
|
|
d5eaaa3527 |
@@ -57,7 +57,7 @@ func NewAdminCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewRepoCommand())
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewDashboardCommand())
|
||||
command.AddCommand(NewDashboardCommand(clientOpts))
|
||||
command.AddCommand(NewNotificationsCommand())
|
||||
command.AddCommand(NewInitialPasswordCommand())
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package admin
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/cmd/argocd/commands/headless"
|
||||
"github.com/argoproj/argo-cd/v2/cmd/argocd/commands/initialize"
|
||||
@@ -14,11 +16,12 @@ import (
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
)
|
||||
|
||||
func NewDashboardCommand() *cobra.Command {
|
||||
func NewDashboardCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
port int
|
||||
address string
|
||||
compressionStr string
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "dashboard",
|
||||
@@ -28,12 +31,13 @@ func NewDashboardCommand() *cobra.Command {
|
||||
|
||||
compression, err := cache.CompressionTypeFromString(compressionStr)
|
||||
errors.CheckError(err)
|
||||
errors.CheckError(headless.StartLocalServer(ctx, &argocdclient.ClientOptions{Core: true}, initialize.RetrieveContextIfChanged(cmd.Flag("context")), &port, &address, compression))
|
||||
clientOpts.Core = true
|
||||
errors.CheckError(headless.MaybeStartLocalServer(ctx, clientOpts, initialize.RetrieveContextIfChanged(cmd.Flag("context")), &port, &address, compression, clientConfig))
|
||||
println(fmt.Sprintf("Argo CD UI is available at http://%s:%d", address, port))
|
||||
<-ctx.Done()
|
||||
},
|
||||
}
|
||||
initialize.InitCommand(cmd)
|
||||
clientConfig = cli.AddKubectlFlagsToSet(cmd.Flags())
|
||||
cmd.Flags().IntVar(&port, "port", common.DefaultPortAPIServer, "Listen on given port")
|
||||
cmd.Flags().StringVar(&address, "address", common.DefaultAddressAdminDashboard, "Listen on given address")
|
||||
cmd.Flags().StringVar(&compressionStr, "redis-compress", env.StringFromEnv("REDIS_COMPRESSION", string(cache.RedisCompressionGZip)), "Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none)")
|
||||
|
||||
@@ -148,13 +148,19 @@ func testAPI(ctx context.Context, clientOpts *apiclient.ClientOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartLocalServer allows executing command in a headless mode: on the fly starts Argo CD API server and
|
||||
// changes provided client options to use started API server port
|
||||
func StartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, compression cache.RedisCompressionType) error {
|
||||
flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError)
|
||||
clientConfig := cli.AddKubectlFlagsToSet(flags)
|
||||
// MaybeStartLocalServer allows executing command in a headless mode. If we're in core mode, starts the Argo CD API
|
||||
// server on the fly and changes provided client options to use started API server port.
|
||||
//
|
||||
// If the clientOpts enables core mode, but the local config does not have core mode enabled, this function will
|
||||
// not start the local server.
|
||||
func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, compression cache.RedisCompressionType, clientConfig clientcmd.ClientConfig) error {
|
||||
if clientConfig == nil {
|
||||
flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError)
|
||||
clientConfig = cli.AddKubectlFlagsToSet(flags)
|
||||
}
|
||||
startInProcessAPI := clientOpts.Core
|
||||
if !startInProcessAPI {
|
||||
// Core mode is enabled on client options. Check the local config to see if we should start the API server.
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading local config: %w", err)
|
||||
@@ -164,9 +170,11 @@ func StartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions,
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving context: %w", err)
|
||||
}
|
||||
// There was a local config file, so determine whether core mode is enabled per the config file.
|
||||
startInProcessAPI = configCtx.Server.Core
|
||||
}
|
||||
}
|
||||
// If we're in core mode, start the API server on the fly.
|
||||
if !startInProcessAPI {
|
||||
return nil
|
||||
}
|
||||
@@ -238,6 +246,7 @@ func StartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions,
|
||||
if !cache2.WaitForCacheSync(ctx.Done(), srv.Initialized) {
|
||||
log.Fatal("Timed out waiting for project cache to sync")
|
||||
}
|
||||
|
||||
tries := 5
|
||||
for i := 0; i < tries; i++ {
|
||||
err = testAPI(ctx, clientOpts)
|
||||
@@ -257,7 +266,9 @@ func NewClientOrDie(opts *apiclient.ClientOptions, c *cobra.Command) apiclient.C
|
||||
ctx := c.Context()
|
||||
|
||||
ctxStr := initialize.RetrieveContextIfChanged(c.Flag("context"))
|
||||
err := StartLocalServer(ctx, opts, ctxStr, nil, nil, cache.RedisCompressionNone)
|
||||
// If we're in core mode, start the API server on the fly and configure the client `opts` to use it.
|
||||
// If we're not in core mode, this function call will do nothing.
|
||||
err := MaybeStartLocalServer(ctx, opts, ctxStr, nil, nil, cache.RedisCompressionNone, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,10 @@ type appStateManager struct {
|
||||
persistResourceHealth bool
|
||||
}
|
||||
|
||||
// getRepoObjs will generate the manifests for the given application delegating the
|
||||
// task to the repo-server. It returns the list of generated manifests as unstructured
|
||||
// objects. It also returns the full response from all calls to the repo server as the
|
||||
// second argument.
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) {
|
||||
|
||||
ts := stats.NewTimingStats()
|
||||
@@ -558,21 +562,16 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
||||
manifestRevisions = append(manifestRevisions, manifestInfo.Revision)
|
||||
}
|
||||
|
||||
// restore comparison using cached diff result if previous comparison was performed for the same revision
|
||||
revisionChanged := len(manifestInfos) != len(sources) || !reflect.DeepEqual(app.Status.Sync.Revisions, manifestRevisions)
|
||||
specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, v1alpha1.ComparedTo{Source: app.Spec.GetSource(), Destination: app.Spec.Destination, Sources: sources, IgnoreDifferences: app.Spec.IgnoreDifferences})
|
||||
|
||||
_, refreshRequested := app.IsRefreshRequested()
|
||||
noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout) || specChanged || revisionChanged
|
||||
useDiffCache := useDiffCache(noCache, manifestInfos, sources, app, manifestRevisions, m.statusRefreshTimeout, logCtx)
|
||||
|
||||
diffConfigBuilder := argodiff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
|
||||
WithTracking(appLabelKey, string(trackingMethod))
|
||||
|
||||
if noCache {
|
||||
diffConfigBuilder.WithNoCache()
|
||||
if useDiffCache {
|
||||
diffConfigBuilder.WithCache(m.cache, app.InstanceName(m.namespace))
|
||||
} else {
|
||||
diffConfigBuilder.WithCache(m.cache, app.GetName())
|
||||
diffConfigBuilder.WithNoCache()
|
||||
}
|
||||
|
||||
gvkParser, err := m.getGVKParser(app.Spec.Destination.Server)
|
||||
@@ -779,6 +778,46 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
||||
return &compRes
|
||||
}
|
||||
|
||||
// useDiffCache will determine if the diff should be calculated based
|
||||
// on the existing live state cache or not.
|
||||
func useDiffCache(noCache bool, manifestInfos []*apiclient.ManifestResponse, sources []v1alpha1.ApplicationSource, app *v1alpha1.Application, manifestRevisions []string, statusRefreshTimeout time.Duration, log *log.Entry) bool {
|
||||
|
||||
if noCache {
|
||||
log.WithField("useDiffCache", "false").Debug("noCache is true")
|
||||
return false
|
||||
}
|
||||
_, refreshRequested := app.IsRefreshRequested()
|
||||
if refreshRequested {
|
||||
log.WithField("useDiffCache", "false").Debug("refreshRequested")
|
||||
return false
|
||||
}
|
||||
if app.Status.Expired(statusRefreshTimeout) {
|
||||
log.WithField("useDiffCache", "false").Debug("app.status.expired")
|
||||
return false
|
||||
}
|
||||
|
||||
if len(manifestInfos) != len(sources) {
|
||||
log.WithField("useDiffCache", "false").Debug("manifestInfos len != sources len")
|
||||
return false
|
||||
}
|
||||
|
||||
revisionChanged := !reflect.DeepEqual(app.Status.GetRevisions(), manifestRevisions)
|
||||
if revisionChanged {
|
||||
log.WithField("useDiffCache", "false").Debug("revisionChanged")
|
||||
return false
|
||||
}
|
||||
|
||||
currentSpec := app.BuildComparedToStatus()
|
||||
specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, currentSpec)
|
||||
if specChanged {
|
||||
log.WithField("useDiffCache", "false").Debug("specChanged")
|
||||
return false
|
||||
}
|
||||
|
||||
log.WithField("useDiffCache", "true").Debug("using diff cache")
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, revisions []string, sources []v1alpha1.ApplicationSource, hasMultipleSources bool, startedAt metav1.Time) error {
|
||||
var nextID int64
|
||||
if len(app.Status.History) > 0 {
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/sirupsen/logrus"
|
||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -1339,3 +1342,252 @@ func TestIsLiveResourceManaged(t *testing.T) {
|
||||
assert.True(t, manager.isSelfReferencedObj(managedWrongAPIGroup, config, appName, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUseDiffCache(t *testing.T) {
|
||||
type fixture struct {
|
||||
testName string
|
||||
noCache bool
|
||||
manifestInfos []*apiclient.ManifestResponse
|
||||
sources []argoappv1.ApplicationSource
|
||||
app *argoappv1.Application
|
||||
manifestRevisions []string
|
||||
statusRefreshTimeout time.Duration
|
||||
expectedUseCache bool
|
||||
}
|
||||
|
||||
manifestInfos := func(revision string) []*apiclient.ManifestResponse {
|
||||
return []*apiclient.ManifestResponse{
|
||||
{
|
||||
Manifests: []string{
|
||||
"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-svc\",\"namespace\":\"httpbin\"},\"spec\":{\"ports\":[{\"name\":\"http-port\",\"port\":7777,\"targetPort\":80},{\"name\":\"test\",\"port\":333}],\"selector\":{\"app\":\"httpbin\"}}}",
|
||||
"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-deployment\",\"namespace\":\"httpbin\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"httpbin\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"httpbin\"}},\"spec\":{\"containers\":[{\"image\":\"kennethreitz/httpbin\",\"imagePullPolicy\":\"Always\",\"name\":\"httpbin\",\"ports\":[{\"containerPort\":80}]}]}}}}",
|
||||
},
|
||||
Namespace: "",
|
||||
Server: "",
|
||||
Revision: revision,
|
||||
SourceType: "Kustomize",
|
||||
VerifyResult: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
sources := func() []argoappv1.ApplicationSource {
|
||||
return []argoappv1.ApplicationSource{
|
||||
{
|
||||
RepoURL: "https://some-repo.com",
|
||||
Path: "argocd/httpbin",
|
||||
TargetRevision: "HEAD",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
app := func(namespace string, revision string, refresh bool, a *argoappv1.Application) *argoappv1.Application {
|
||||
app := &argoappv1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "httpbin",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: argoappv1.ApplicationSpec{
|
||||
Source: &argoappv1.ApplicationSource{
|
||||
RepoURL: "https://some-repo.com",
|
||||
Path: "argocd/httpbin",
|
||||
TargetRevision: "HEAD",
|
||||
},
|
||||
Destination: argoappv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "httpbin",
|
||||
},
|
||||
Project: "default",
|
||||
SyncPolicy: &argoappv1.SyncPolicy{
|
||||
SyncOptions: []string{
|
||||
"CreateNamespace=true",
|
||||
"ServerSideApply=true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: argoappv1.ApplicationStatus{
|
||||
Resources: []argoappv1.ResourceStatus{},
|
||||
Sync: argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeSynced,
|
||||
ComparedTo: argoappv1.ComparedTo{
|
||||
Source: argoappv1.ApplicationSource{
|
||||
RepoURL: "https://some-repo.com",
|
||||
Path: "argocd/httpbin",
|
||||
TargetRevision: "HEAD",
|
||||
},
|
||||
Destination: argoappv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "httpbin",
|
||||
},
|
||||
},
|
||||
Revision: revision,
|
||||
Revisions: []string{},
|
||||
},
|
||||
ReconciledAt: &metav1.Time{
|
||||
Time: time.Now().Add(-time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
if refresh {
|
||||
annotations := make(map[string]string)
|
||||
annotations[argoappv1.AnnotationKeyRefresh] = string(argoappv1.RefreshTypeNormal)
|
||||
app.SetAnnotations(annotations)
|
||||
}
|
||||
if a != nil {
|
||||
err := mergo.Merge(app, a, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue)
|
||||
if err != nil {
|
||||
t.Fatalf("error merging app: %s", err)
|
||||
}
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
cases := []fixture{
|
||||
{
|
||||
testName: "will use diff cache",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", false, nil),
|
||||
manifestRevisions: []string{"rev1"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: true,
|
||||
},
|
||||
{
|
||||
testName: "will use diff cache for multisource",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "", false, &argoappv1.Application{
|
||||
Spec: argoappv1.ApplicationSpec{
|
||||
Source: nil,
|
||||
Sources: argoappv1.ApplicationSources{
|
||||
{
|
||||
RepoURL: "multisource repo1",
|
||||
},
|
||||
{
|
||||
RepoURL: "multisource repo2",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: argoappv1.ApplicationStatus{
|
||||
Resources: []argoappv1.ResourceStatus{},
|
||||
Sync: argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeSynced,
|
||||
ComparedTo: argoappv1.ComparedTo{
|
||||
Source: argoappv1.ApplicationSource{},
|
||||
Sources: argoappv1.ApplicationSources{
|
||||
{
|
||||
RepoURL: "multisource repo1",
|
||||
},
|
||||
{
|
||||
RepoURL: "multisource repo2",
|
||||
},
|
||||
},
|
||||
},
|
||||
Revisions: []string{"rev1", "rev2"},
|
||||
},
|
||||
ReconciledAt: &metav1.Time{
|
||||
Time: time.Now().Add(-time.Hour),
|
||||
},
|
||||
},
|
||||
}),
|
||||
manifestRevisions: []string{"rev1", "rev2"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: true,
|
||||
},
|
||||
{
|
||||
testName: "will return false if nocache is true",
|
||||
noCache: true,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", false, nil),
|
||||
manifestRevisions: []string{"rev1"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: false,
|
||||
},
|
||||
{
|
||||
testName: "will return false if requested refresh",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", true, nil),
|
||||
manifestRevisions: []string{"rev1"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: false,
|
||||
},
|
||||
{
|
||||
testName: "will return false if status expired",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", false, nil),
|
||||
manifestRevisions: []string{"rev1"},
|
||||
statusRefreshTimeout: time.Minute,
|
||||
expectedUseCache: false,
|
||||
},
|
||||
{
|
||||
testName: "will return false if there is a new revision",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", false, nil),
|
||||
manifestRevisions: []string{"rev2"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: false,
|
||||
},
|
||||
{
|
||||
testName: "will return false if app spec repo changed",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", false, &argoappv1.Application{
|
||||
Spec: argoappv1.ApplicationSpec{
|
||||
Source: &argoappv1.ApplicationSource{
|
||||
RepoURL: "new-repo",
|
||||
},
|
||||
},
|
||||
}),
|
||||
manifestRevisions: []string{"rev1"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: false,
|
||||
},
|
||||
{
|
||||
testName: "will return false if app spec IgnoreDifferences changed",
|
||||
noCache: false,
|
||||
manifestInfos: manifestInfos("rev1"),
|
||||
sources: sources(),
|
||||
app: app("httpbin", "rev1", false, &argoappv1.Application{
|
||||
Spec: argoappv1.ApplicationSpec{
|
||||
IgnoreDifferences: []argoappv1.ResourceIgnoreDifferences{
|
||||
{
|
||||
Group: "app/v1",
|
||||
Kind: "application",
|
||||
Name: "httpbin",
|
||||
Namespace: "httpbin",
|
||||
JQPathExpressions: []string{"."},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
manifestRevisions: []string{"rev1"},
|
||||
statusRefreshTimeout: time.Hour * 24,
|
||||
expectedUseCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
// Given
|
||||
t.Parallel()
|
||||
logger, _ := logrustest.NewNullLogger()
|
||||
log := logrus.NewEntry(logger)
|
||||
|
||||
// When
|
||||
useDiffCache := useDiffCache(tc.noCache, tc.manifestInfos, tc.sources, tc.app, tc.manifestRevisions, tc.statusRefreshTimeout, log)
|
||||
|
||||
// Then
|
||||
assert.Equal(t, useDiffCache, tc.expectedUseCache)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ argocd admin dashboard [flags]
|
||||
--proxy-url string If provided, this URL will be used to connect via proxy
|
||||
--redis-compress string Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none) (default "gzip")
|
||||
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
|
||||
--server string The address and port of the Kubernetes API server
|
||||
--tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used.
|
||||
--token string Bearer token for authentication to the API server
|
||||
--user string The name of the kubeconfig user to use
|
||||
@@ -58,7 +59,6 @@ argocd admin dashboard [flags]
|
||||
--redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy")
|
||||
--redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis")
|
||||
--repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server")
|
||||
--server string Argo CD server address
|
||||
--server-crt string Server certificate file
|
||||
--server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server")
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ kind: Kustomization
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.9.2
|
||||
newTag: v2.9.3
|
||||
resources:
|
||||
- ./application-controller
|
||||
- ./dex
|
||||
|
||||
@@ -20742,7 +20742,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -21042,7 +21042,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -21094,7 +21094,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -21313,7 +21313,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.9.2
|
||||
newTag: v2.9.3
|
||||
|
||||
@@ -12,7 +12,7 @@ patches:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.9.2
|
||||
newTag: v2.9.3
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/applicationset-controller
|
||||
|
||||
@@ -21999,7 +21999,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -22122,7 +22122,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -22198,7 +22198,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -22529,7 +22529,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -22581,7 +22581,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -22870,7 +22870,7 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -23116,7 +23116,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -1654,7 +1654,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -1777,7 +1777,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -1853,7 +1853,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -2184,7 +2184,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -2236,7 +2236,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -2525,7 +2525,7 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -2771,7 +2771,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -21094,7 +21094,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -21217,7 +21217,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -21293,7 +21293,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -21575,7 +21575,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -21627,7 +21627,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -21914,7 +21914,7 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -22160,7 +22160,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -749,7 +749,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -872,7 +872,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -948,7 +948,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1230,7 +1230,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -1282,7 +1282,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -1569,7 +1569,7 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -1815,7 +1815,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.2
|
||||
image: quay.io/argoproj/argocd:v2.9.3
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -951,6 +951,35 @@ type ApplicationStatus struct {
|
||||
ControllerNamespace string `json:"controllerNamespace,omitempty" protobuf:"bytes,13,opt,name=controllerNamespace"`
|
||||
}
|
||||
|
||||
// GetRevisions will return the current revision associated with the Application.
|
||||
// If app has multisources, it will return all corresponding revisions preserving
|
||||
// order from the app.spec.sources. If app has only one source, it will return a
|
||||
// single revision in the list.
|
||||
func (a *ApplicationStatus) GetRevisions() []string {
|
||||
revisions := []string{}
|
||||
if len(a.Sync.Revisions) > 0 {
|
||||
revisions = a.Sync.Revisions
|
||||
} else if a.Sync.Revision != "" {
|
||||
revisions = append(revisions, a.Sync.Revision)
|
||||
}
|
||||
return revisions
|
||||
}
|
||||
|
||||
// BuildComparedToStatus will build a ComparedTo object based on the current
|
||||
// Application state.
|
||||
func (app *Application) BuildComparedToStatus() ComparedTo {
|
||||
ct := ComparedTo{
|
||||
Destination: app.Spec.Destination,
|
||||
IgnoreDifferences: app.Spec.IgnoreDifferences,
|
||||
}
|
||||
if app.Spec.HasMultipleSources() {
|
||||
ct.Sources = app.Spec.Sources
|
||||
} else {
|
||||
ct.Source = app.Spec.GetSource()
|
||||
}
|
||||
return ct
|
||||
}
|
||||
|
||||
// JWTTokens represents a list of JWT tokens
|
||||
type JWTTokens struct {
|
||||
Items []JWTToken `json:"items,omitempty" protobuf:"bytes,1,opt,name=items"`
|
||||
|
||||
71
reposerver/cache/mocks/reposervercache.go
vendored
Normal file
71
reposerver/cache/mocks/reposervercache.go
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
cacheutilmocks "github.com/argoproj/argo-cd/v2/util/cache/mocks"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockCacheType int
|
||||
|
||||
const (
|
||||
MockCacheTypeRedis MockCacheType = iota
|
||||
MockCacheTypeInMem
|
||||
)
|
||||
|
||||
type MockRepoCache struct {
|
||||
mock.Mock
|
||||
RedisClient *cacheutilmocks.MockCacheClient
|
||||
StopRedisCallback func()
|
||||
}
|
||||
|
||||
type MockCacheOptions struct {
|
||||
RepoCacheExpiration time.Duration
|
||||
RevisionCacheExpiration time.Duration
|
||||
ReadDelay time.Duration
|
||||
WriteDelay time.Duration
|
||||
}
|
||||
|
||||
type CacheCallCounts struct {
|
||||
ExternalSets int
|
||||
ExternalGets int
|
||||
ExternalDeletes int
|
||||
}
|
||||
|
||||
// Checks that the cache was called the expected number of times
|
||||
func (mockCache *MockRepoCache) AssertCacheCalledTimes(t *testing.T, calls *CacheCallCounts) {
|
||||
mockCache.RedisClient.AssertNumberOfCalls(t, "Get", calls.ExternalGets)
|
||||
mockCache.RedisClient.AssertNumberOfCalls(t, "Set", calls.ExternalSets)
|
||||
mockCache.RedisClient.AssertNumberOfCalls(t, "Delete", calls.ExternalDeletes)
|
||||
}
|
||||
|
||||
func (mockCache *MockRepoCache) ConfigureDefaultCallbacks() {
|
||||
mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Return(nil)
|
||||
mockCache.RedisClient.On("Set", mock.Anything).Return(nil)
|
||||
mockCache.RedisClient.On("Delete", mock.Anything).Return(nil)
|
||||
}
|
||||
|
||||
func NewInMemoryRedis() (*redis.Client, func()) {
|
||||
cacheutil.NewInMemoryCache(5 * time.Second)
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return redis.NewClient(&redis.Options{Addr: mr.Addr()}), mr.Close
|
||||
}
|
||||
|
||||
func NewMockRepoCache(cacheOpts *MockCacheOptions) *MockRepoCache {
|
||||
redisClient, stopRedis := NewInMemoryRedis()
|
||||
redisCacheClient := &cacheutilmocks.MockCacheClient{
|
||||
ReadDelay: cacheOpts.ReadDelay,
|
||||
WriteDelay: cacheOpts.WriteDelay,
|
||||
BaseCache: cacheutil.NewRedisCache(redisClient, cacheOpts.RepoCacheExpiration, cacheutil.RedisCompressionNone)}
|
||||
newMockCache := &MockRepoCache{RedisClient: redisCacheClient, StopRedisCallback: stopRedis}
|
||||
newMockCache.ConfigureDefaultCallbacks()
|
||||
return newMockCache
|
||||
}
|
||||
@@ -300,6 +300,7 @@ func (s *Service) runRepoOperation(
|
||||
var gitClient git.Client
|
||||
var helmClient helm.Client
|
||||
var err error
|
||||
gitClientOpts := git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache)
|
||||
revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
|
||||
unresolvedRevision := revision
|
||||
if source.IsHelm() {
|
||||
@@ -308,13 +309,13 @@ func (s *Service) runRepoOperation(
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
gitClient, revision, err = s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache))
|
||||
gitClient, revision, err = s.newClientResolveRevision(repo, revision, gitClientOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision)
|
||||
repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision, gitClientOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -463,7 +464,7 @@ type gitClientGetter func(repo *v1alpha1.Repository, revision string, opts ...gi
|
||||
//
|
||||
// Much of this logic is duplicated in runManifestGenAsync. If making changes here, check whether runManifestGenAsync
|
||||
// should be updated.
|
||||
func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter) (map[string]string, error) {
|
||||
func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter, gitClientOpts git.ClientOpts) (map[string]string, error) {
|
||||
repoRefs := make(map[string]string)
|
||||
if !hasMultipleSources || source == nil {
|
||||
return repoRefs, nil
|
||||
@@ -490,7 +491,7 @@ func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.Applicat
|
||||
normalizedRepoURL := git.NormalizeGitURL(refSourceMapping.Repo.Repo)
|
||||
_, ok = repoRefs[normalizedRepoURL]
|
||||
if !ok {
|
||||
_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision)
|
||||
_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, gitClientOpts)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
|
||||
return nil, fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
|
||||
@@ -728,7 +729,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
|
||||
return
|
||||
}
|
||||
} else {
|
||||
gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision)
|
||||
gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
|
||||
ch.errCh <- fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
@@ -28,13 +30,14 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
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/reposerver/cache"
|
||||
repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/metrics"
|
||||
fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks"
|
||||
@@ -51,12 +54,49 @@ gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>"
|
||||
|
||||
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths)
|
||||
|
||||
func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) {
|
||||
type repoCacheMocks struct {
|
||||
mock.Mock
|
||||
cacheutilCache *cacheutil.Cache
|
||||
cache *cache.Cache
|
||||
mockCache *repositorymocks.MockRepoCache
|
||||
}
|
||||
|
||||
type newGitRepoHelmChartOptions struct {
|
||||
chartName string
|
||||
chartVersion string
|
||||
// valuesFiles is a map of the values file name to the key/value pairs to be written to the file
|
||||
valuesFiles map[string]map[string]string
|
||||
}
|
||||
|
||||
type newGitRepoOptions struct {
|
||||
path string
|
||||
createPath bool
|
||||
remote string
|
||||
addEmptyCommit bool
|
||||
helmChartOptions newGitRepoHelmChartOptions
|
||||
}
|
||||
|
||||
func newCacheMocks() *repoCacheMocks {
|
||||
mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{
|
||||
RepoCacheExpiration: 1 * time.Minute,
|
||||
RevisionCacheExpiration: 1 * time.Minute,
|
||||
ReadDelay: 0,
|
||||
WriteDelay: 0,
|
||||
})
|
||||
cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient)
|
||||
return &repoCacheMocks{
|
||||
cacheutilCache: cacheutilCache,
|
||||
cache: cache.NewCache(cacheutilCache, 1*time.Minute, 1*time.Minute),
|
||||
mockCache: mockRepoCache,
|
||||
}
|
||||
}
|
||||
|
||||
func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
||||
root, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
@@ -73,7 +113,7 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
|
||||
chart := "my-chart"
|
||||
oobChart := "out-of-bounds-chart"
|
||||
version := "1.1.0"
|
||||
helmClient.On("GetIndex", true).Return(&helm.Index{Entries: map[string]helm.Entries{
|
||||
helmClient.On("GetIndex", mock.AnythingOfType("bool")).Return(&helm.Index{Entries: map[string]helm.Entries{
|
||||
chart: {{Version: "1.0.0"}, {Version: version}},
|
||||
oobChart: {{Version: "1.0.0"}, {Version: version}},
|
||||
}}, nil)
|
||||
@@ -89,18 +129,16 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
|
||||
}, root)
|
||||
}
|
||||
|
||||
func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client) {
|
||||
func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
||||
helmClient := &helmmocks.Client{}
|
||||
gitClient := &gitmocks.Client{}
|
||||
paths := &iomocks.TempPaths{}
|
||||
cf(gitClient, helmClient, paths)
|
||||
service := NewService(metrics.NewMetricsServer(), cache.NewCache(
|
||||
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
|
||||
1*time.Minute,
|
||||
1*time.Minute,
|
||||
), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
|
||||
cacheMocks := newCacheMocks()
|
||||
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
||||
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
|
||||
|
||||
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, prosy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
||||
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
||||
return gitClient, nil
|
||||
}
|
||||
service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
|
||||
@@ -110,20 +148,20 @@ func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client)
|
||||
return io.NopCloser
|
||||
}
|
||||
service.gitRepoPaths = paths
|
||||
return service, gitClient
|
||||
return service, gitClient, cacheMocks
|
||||
}
|
||||
|
||||
func newService(root string) *Service {
|
||||
service, _ := newServiceWithMocks(root, false)
|
||||
func newService(t *testing.T, root string) *Service {
|
||||
service, _, _ := newServiceWithMocks(t, root, false)
|
||||
return service
|
||||
}
|
||||
|
||||
func newServiceWithSignature(root string) *Service {
|
||||
service, _ := newServiceWithMocks(root, true)
|
||||
func newServiceWithSignature(t *testing.T, root string) *Service {
|
||||
service, _, _ := newServiceWithMocks(t, root, true)
|
||||
return service
|
||||
}
|
||||
|
||||
func newServiceWithCommitSHA(root, revision string) *Service {
|
||||
func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
|
||||
var revisionErr error
|
||||
|
||||
commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$")
|
||||
@@ -131,7 +169,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
|
||||
revisionErr = errors.New("not a commit SHA")
|
||||
}
|
||||
|
||||
service, gitClient := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
@@ -150,7 +188,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
|
||||
}
|
||||
|
||||
func TestGenerateYamlManifestInDir(t *testing.T) {
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
@@ -247,7 +285,7 @@ func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
@@ -275,7 +313,7 @@ func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateManifests_EmptyCache(t *testing.T) {
|
||||
service := newService("../../manifests/base")
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
@@ -291,11 +329,85 @@ func TestGenerateManifests_EmptyCache(t *testing.T) {
|
||||
res, err := service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(res.Manifests) > 0)
|
||||
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 2,
|
||||
ExternalGets: 2,
|
||||
ExternalDeletes: 1})
|
||||
gitMocks.AssertCalled(t, "LsRemote", mock.Anything)
|
||||
gitMocks.AssertCalled(t, "Fetch", mock.Anything)
|
||||
}
|
||||
|
||||
// Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote
|
||||
func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repopath := fmt.Sprintf("%s/tmprepo", dir)
|
||||
cacheMocks := newCacheMocks()
|
||||
t.Cleanup(func() {
|
||||
cacheMocks.mockCache.StopRedisCallback()
|
||||
err := filepath.WalkDir(dir,
|
||||
func(path string, di fs.DirEntry, err error) error {
|
||||
if err == nil {
|
||||
return os.Chmod(path, 0777)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath)
|
||||
var gitClient git.Client
|
||||
var err error
|
||||
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
||||
opts = append(opts, git.WithEventHandlers(git.EventHandlers{
|
||||
// Primary check, we want to make sure ls-remote is not called when the item is in cache
|
||||
OnLsRemote: func(repo string) func() {
|
||||
return func() {
|
||||
assert.Fail(t, "LsRemote should not be called when the item is in cache")
|
||||
}
|
||||
},
|
||||
}))
|
||||
gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...)
|
||||
return gitClient, err
|
||||
}
|
||||
repoRemote := fmt.Sprintf("file://%s", repopath)
|
||||
revision := initGitRepo(t, newGitRepoOptions{
|
||||
path: repopath,
|
||||
createPath: true,
|
||||
remote: repoRemote,
|
||||
helmChartOptions: newGitRepoHelmChartOptions{
|
||||
chartName: "my-chart",
|
||||
chartVersion: "v1.0.0",
|
||||
valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}}},
|
||||
})
|
||||
src := argoappv1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &argoappv1.ApplicationSourceHelm{
|
||||
ValueFiles: []string{"$ref/test.yaml"},
|
||||
}}
|
||||
repo := &argoappv1.Repository{
|
||||
Repo: repoRemote,
|
||||
}
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Revision: "HEAD",
|
||||
HasMultipleSources: true,
|
||||
ApplicationSource: &src,
|
||||
ProjectName: "default",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
RefSources: map[string]*argoappv1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}},
|
||||
}
|
||||
err = cacheMocks.cacheutilCache.SetItem(fmt.Sprintf("git-refs|%s", repoRemote), [][2]string{{"HEAD", revision}}, 30*time.Second, false)
|
||||
assert.NoError(t, err)
|
||||
_, err = service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 2,
|
||||
ExternalGets: 5})
|
||||
}
|
||||
|
||||
// ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
|
||||
func TestHelmManifestFromChartRepo(t *testing.T) {
|
||||
service := newService(".")
|
||||
root := t.TempDir()
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
|
||||
source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
|
||||
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"}}
|
||||
@@ -309,10 +421,14 @@ func TestHelmManifestFromChartRepo(t *testing.T) {
|
||||
Revision: "1.1.0",
|
||||
SourceType: "Helm",
|
||||
}, response)
|
||||
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 1,
|
||||
ExternalGets: 0})
|
||||
gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything)
|
||||
}
|
||||
|
||||
func TestHelmChartReferencingExternalValues(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
spec := argoappv1.ApplicationSpec{
|
||||
Sources: []argoappv1.ApplicationSource{
|
||||
{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{
|
||||
@@ -342,7 +458,7 @@ func TestHelmChartReferencingExternalValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
err := os.Mkdir("testdata/oob-symlink", 0755)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
@@ -376,7 +492,7 @@ func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
||||
service, gitClient := newServiceWithMocks(".", false)
|
||||
service, gitClient, _ := newServiceWithMocks(t, ".", false)
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
@@ -390,7 +506,7 @@ func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecurseManifestsInDir(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
@@ -403,7 +519,7 @@ func TestRecurseManifestsInDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidManifestsInDir(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
@@ -414,7 +530,7 @@ func TestInvalidManifestsInDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidMetadata(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"}
|
||||
@@ -424,7 +540,7 @@ func TestInvalidMetadata(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNilMetadataAccessors(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}"
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
@@ -436,7 +552,7 @@ func TestNilMetadataAccessors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateJsonnetManifestInDir(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -459,7 +575,7 @@ func TestGenerateJsonnetManifestInDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
|
||||
service := newService("testdata/jsonnet-1")
|
||||
service := newService(t, "testdata/jsonnet-1")
|
||||
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -482,7 +598,7 @@ func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateJsonnetLibOutside(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -553,7 +669,7 @@ func TestManifestGenErrorCacheByNumRequests(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations)
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
service.initConstants = RepoServerInitConstants{
|
||||
ParallelismLimit: 1,
|
||||
@@ -631,7 +747,7 @@ func TestManifestGenErrorCacheFileContentsChange(t *testing.T) {
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
service := newService(tmpDir)
|
||||
service := newService(t, tmpDir)
|
||||
|
||||
service.initConstants = RepoServerInitConstants{
|
||||
ParallelismLimit: 1,
|
||||
@@ -701,7 +817,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes)
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
// Here we simulate the passage of time by overriding the now() function of Service
|
||||
currentTime := time.Now()
|
||||
@@ -771,7 +887,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
|
||||
|
||||
func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
service.initConstants = RepoServerInitConstants{
|
||||
ParallelismLimit: 1,
|
||||
@@ -828,7 +944,7 @@ func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateHelmWithValues(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -865,7 +981,7 @@ func TestGenerateHelmWithValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHelmWithMissingValueFiles(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
missingValuesFile := "values-prod-overrides.yaml"
|
||||
|
||||
req := &apiclient.ManifestRequest{
|
||||
@@ -893,7 +1009,7 @@ func TestHelmWithMissingValueFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateHelmWithEnvVars(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -930,7 +1046,7 @@ func TestGenerateHelmWithEnvVars(t *testing.T) {
|
||||
// The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however
|
||||
// since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed
|
||||
func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata")
|
||||
service := newService(t, "../../util/helm/testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -947,7 +1063,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test the case where the path is "."
|
||||
service = newService("./testdata")
|
||||
service = newService(t, "./testdata")
|
||||
_, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -961,7 +1077,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"}
|
||||
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true}
|
||||
_, err := service.GenerateManifest(context.Background(), request)
|
||||
@@ -971,7 +1087,7 @@ func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
|
||||
// This is a Helm first-class app with a values file inside the repo directory
|
||||
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed
|
||||
func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Chart: "my-chart",
|
||||
TargetRevision: ">= 1.0.0",
|
||||
@@ -1000,7 +1116,7 @@ func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
|
||||
// This is a Helm first-class app with a values file outside the repo directory
|
||||
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed
|
||||
func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Chart: "my-chart",
|
||||
TargetRevision: ">= 1.0.0",
|
||||
@@ -1015,7 +1131,7 @@ func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
|
||||
|
||||
func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
|
||||
t.Run("Valid symlink", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Chart: "my-chart",
|
||||
TargetRevision: ">= 1.0.0",
|
||||
@@ -1031,7 +1147,7 @@ func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateHelmWithURL(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1054,7 +1170,7 @@ func TestGenerateHelmWithURL(t *testing.T) {
|
||||
// (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked
|
||||
func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -1073,7 +1189,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -1091,7 +1207,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -1109,7 +1225,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -1128,7 +1244,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -1147,7 +1263,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Remote values file from custom allowed protocol", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
@@ -1168,7 +1284,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
|
||||
// File parameter should not allow traversal outside of the repository root
|
||||
func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
|
||||
service := newService("../..")
|
||||
service := newService(t, "../..")
|
||||
|
||||
file, err := os.CreateTemp("", "external-secret.txt")
|
||||
assert.NoError(t, err)
|
||||
@@ -1209,7 +1325,7 @@ func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
|
||||
// directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of
|
||||
// providing direct content to a helm chart via a specific key.
|
||||
func TestGenerateHelmWithFileParameter(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata")
|
||||
service := newService(t, "../../util/helm/testdata")
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1234,7 +1350,7 @@ func TestGenerateHelmWithFileParameter(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateNullList(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
t.Run("null list", func(t *testing.T) {
|
||||
res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
@@ -1302,7 +1418,7 @@ func TestGenerateFromUTF16(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListApps(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
|
||||
res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}})
|
||||
assert.NoError(t, err)
|
||||
@@ -1329,7 +1445,7 @@ func TestListApps(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAppDetailsHelm(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/dependency")
|
||||
service := newService(t, "../../util/helm/testdata/dependency")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1344,8 +1460,26 @@ func TestGetAppDetailsHelm(t *testing.T) {
|
||||
assert.Equal(t, "Helm", res.Type)
|
||||
assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
|
||||
}
|
||||
|
||||
func TestGetAppDetailsHelmUsesCache(t *testing.T) {
|
||||
service := newService(t, "../../util/helm/testdata/dependency")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
Source: &argoappv1.ApplicationSource{
|
||||
Path: ".",
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res.Helm)
|
||||
|
||||
assert.Equal(t, "Helm", res.Type)
|
||||
assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
|
||||
}
|
||||
|
||||
func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/api-versions")
|
||||
service := newService(t, "../../util/helm/testdata/api-versions")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1363,7 +1497,7 @@ func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAppDetailsKustomize(t *testing.T) {
|
||||
service := newService("../../util/kustomize/testdata/kustomization_yaml")
|
||||
service := newService(t, "../../util/kustomize/testdata/kustomization_yaml")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1380,7 +1514,7 @@ func TestGetAppDetailsKustomize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetHelmCharts(t *testing.T) {
|
||||
service := newService("../..")
|
||||
service := newService(t, "../..")
|
||||
res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}})
|
||||
|
||||
// fix flakiness
|
||||
@@ -1401,7 +1535,7 @@ func TestGetHelmCharts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetRevisionMetadata(t *testing.T) {
|
||||
service, gitClient := newServiceWithMocks("../..", false)
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../..", false)
|
||||
now := time.Now()
|
||||
|
||||
gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
|
||||
@@ -1469,7 +1603,7 @@ func TestGetRevisionMetadata(t *testing.T) {
|
||||
func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
// Commit with signature and verification requested
|
||||
{
|
||||
service := newServiceWithSignature("../../manifests/base")
|
||||
service := newServiceWithSignature(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
@@ -1486,7 +1620,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
}
|
||||
// Commit with signature and verification not requested
|
||||
{
|
||||
service := newServiceWithSignature("../../manifests/base")
|
||||
service := newServiceWithSignature(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something",
|
||||
@@ -1498,7 +1632,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
}
|
||||
// Commit without signature and verification requested
|
||||
{
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
||||
@@ -1510,7 +1644,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
}
|
||||
// Commit without signature and verification not requested
|
||||
{
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
||||
@@ -1543,7 +1677,7 @@ func Test_newEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_newHelmClientResolveRevision(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
t.Run("EmptyRevision", func(t *testing.T) {
|
||||
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true)
|
||||
@@ -1557,7 +1691,7 @@ func TestService_newHelmClientResolveRevision(t *testing.T) {
|
||||
|
||||
func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
t.Run("No app name set and app specific file exists", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1570,7 +1704,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("No app specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1584,7 +1718,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("Only app specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1598,7 +1732,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("App specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1612,7 +1746,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1626,7 +1760,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
})
|
||||
t.Run("Broken app-specific overrides", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
_, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1668,7 +1802,7 @@ func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, pa
|
||||
func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
||||
t.Run("Single global override", func(t *testing.T) {
|
||||
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
ApplicationSource: &argoappv1.ApplicationSource{
|
||||
@@ -1699,7 +1833,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
||||
|
||||
t.Run("Single global override Helm", func(t *testing.T) {
|
||||
runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
ApplicationSource: &argoappv1.ApplicationSource{
|
||||
@@ -1729,7 +1863,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Application specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1760,8 +1894,29 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) {
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
ApplicationSource: &argoappv1.ApplicationSource{
|
||||
Path: "",
|
||||
Chart: "",
|
||||
Ref: "test",
|
||||
},
|
||||
AppName: "testapp-multi-ref-only",
|
||||
ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
HasMultipleSources: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, manifests.Manifests)
|
||||
assert.NotEmpty(t, manifests.Revision)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Application specific override for other app", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
@@ -1793,7 +1948,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Override info does not appear in cache key", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Path: path,
|
||||
@@ -1843,7 +1998,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
},
|
||||
wantError: false,
|
||||
service: newServiceWithCommitSHA(".", regularGitTagHash),
|
||||
service: newServiceWithCommitSHA(t, ".", regularGitTagHash),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -1859,7 +2014,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
},
|
||||
wantError: false,
|
||||
service: newServiceWithCommitSHA(".", annotatedGitTaghash),
|
||||
service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -1875,7 +2030,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
},
|
||||
wantError: true,
|
||||
service: newServiceWithCommitSHA(".", invalidGitTaghash),
|
||||
service: newServiceWithCommitSHA(t, ".", invalidGitTaghash),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -1900,7 +2055,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
||||
func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) {
|
||||
annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
|
||||
|
||||
service := newServiceWithCommitSHA(".", annotatedGitTaghash)
|
||||
service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash)
|
||||
|
||||
refSources := map[string]*argoappv1.RefTarget{}
|
||||
|
||||
@@ -2485,7 +2640,7 @@ func Test_findManifests(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTestRepoOCI(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
_, err := service.TestRepository(context.Background(), &apiclient.TestRepositoryRequest{
|
||||
Repo: &argoappv1.Repository{
|
||||
Repo: "https://demo.goharbor.io",
|
||||
@@ -2510,7 +2665,7 @@ func Test_getHelmDependencyRepos(t *testing.T) {
|
||||
|
||||
func TestResolveRevision(t *testing.T) {
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
|
||||
app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
|
||||
resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
|
||||
@@ -2532,7 +2687,7 @@ func TestResolveRevision(t *testing.T) {
|
||||
|
||||
func TestResolveRevisionNegativeScenarios(t *testing.T) {
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
|
||||
app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
|
||||
resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
|
||||
@@ -2579,19 +2734,57 @@ func TestDirectoryPermissionInitializer(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func initGitRepo(repoPath string, remote string) error {
|
||||
if err := os.Mkdir(repoPath, 0755); err != nil {
|
||||
return err
|
||||
func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) {
|
||||
err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0777)
|
||||
assert.NoError(t, err)
|
||||
for valuesFileName, values := range options.helmChartOptions.valuesFiles {
|
||||
valuesFileContents, err := yaml.Marshal(values)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0777)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
cmd := exec.Command("git", "add", "-A")
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
cmd = exec.Command("git", "commit", "-m", "Initial commit")
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
}
|
||||
|
||||
func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) {
|
||||
if options.createPath {
|
||||
assert.NoError(t, os.Mkdir(options.path, 0755))
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", "init", repoPath)
|
||||
cmd.Dir = repoPath
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
cmd := exec.Command("git", "init", options.path)
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
if options.remote != "" {
|
||||
cmd = exec.Command("git", "remote", "add", "origin", options.path)
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
}
|
||||
cmd = exec.Command("git", "remote", "add", "origin", remote)
|
||||
cmd.Dir = repoPath
|
||||
return cmd.Run()
|
||||
|
||||
commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != ""
|
||||
if options.addEmptyCommit {
|
||||
cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty")
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
} else if options.helmChartOptions.chartName != "" {
|
||||
addHelmToGitRepo(t, options)
|
||||
}
|
||||
|
||||
if commitAdded {
|
||||
var revB bytes.Buffer
|
||||
cmd = exec.Command("git", "rev-parse", "HEAD", options.path)
|
||||
cmd.Dir = options.path
|
||||
cmd.Stdout = &revB
|
||||
assert.NoError(t, cmd.Run())
|
||||
revision = strings.Split(revB.String(), "\n")[0]
|
||||
}
|
||||
return revision
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
@@ -2604,16 +2797,16 @@ func TestInit(t *testing.T) {
|
||||
})
|
||||
|
||||
repoPath := path.Join(dir, "repo1")
|
||||
require.NoError(t, initGitRepo(repoPath, "https://github.com/argo-cd/test-repo1"))
|
||||
initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false})
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
service.rootDir = dir
|
||||
|
||||
require.NoError(t, service.Init())
|
||||
|
||||
_, err := os.ReadDir(dir)
|
||||
require.Error(t, err)
|
||||
require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2"))
|
||||
initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false})
|
||||
}
|
||||
|
||||
// TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
|
||||
@@ -2915,7 +3108,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
||||
want *apiclient.GitDirectoriesResponse
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
||||
ctx: context.TODO(),
|
||||
request: &apiclient.GitDirectoriesRequest{
|
||||
Repo: nil,
|
||||
@@ -2924,7 +3117,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
||||
},
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -2955,7 +3148,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
||||
func TestGetGitDirectories(t *testing.T) {
|
||||
// test not using the cache
|
||||
root := "./testdata/git-files-dirs"
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
@@ -2978,6 +3171,10 @@ func TestGetGitDirectories(t *testing.T) {
|
||||
directories, err = s.GetGitDirectories(context.TODO(), dirRequest)
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths())
|
||||
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 1,
|
||||
ExternalGets: 2,
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorGetGitFiles(t *testing.T) {
|
||||
@@ -2995,7 +3192,7 @@ func TestErrorGetGitFiles(t *testing.T) {
|
||||
want *apiclient.GitFilesResponse
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
||||
ctx: context.TODO(),
|
||||
request: &apiclient.GitFilesRequest{
|
||||
Repo: nil,
|
||||
@@ -3004,7 +3201,7 @@ func TestErrorGetGitFiles(t *testing.T) {
|
||||
},
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3037,7 +3234,7 @@ func TestGetGitFiles(t *testing.T) {
|
||||
files := []string{"./testdata/git-files-dirs/somedir/config.yaml",
|
||||
"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml"}
|
||||
root := ""
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
@@ -3070,6 +3267,10 @@ func TestGetGitFiles(t *testing.T) {
|
||||
fileResponse, err = s.GetGitFiles(context.TODO(), filesRequest)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, fileResponse.GetMap())
|
||||
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 1,
|
||||
ExternalGets: 2,
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getRepoSanitizerRegex(t *testing.T) {
|
||||
@@ -3079,3 +3280,45 @@ func Test_getRepoSanitizerRegex(t *testing.T) {
|
||||
msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>")
|
||||
assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg)
|
||||
}
|
||||
|
||||
func TestGetRevisionChartDetails(t *testing.T) {
|
||||
t.Run("Test revision semvar", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
service := newService(t, root)
|
||||
_, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: fmt.Sprintf("file://%s", root),
|
||||
Name: "test-repo-name",
|
||||
Type: "helm",
|
||||
},
|
||||
Name: "test-name",
|
||||
Revision: "test-revision",
|
||||
})
|
||||
assert.ErrorContains(t, err, "invalid revision")
|
||||
})
|
||||
|
||||
t.Run("Test GetRevisionChartDetails", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
service := newService(t, root)
|
||||
repoUrl := fmt.Sprintf("file://%s", root)
|
||||
err := service.cache.SetRevisionChartDetails(repoUrl, "my-chart", "1.1.0", &argoappv1.ChartDetails{
|
||||
Description: "test-description",
|
||||
Home: "test-home",
|
||||
Maintainers: []string{"test-maintainer"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
chartDetails, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: fmt.Sprintf("file://%s", root),
|
||||
Name: "test-repo-name",
|
||||
Type: "helm",
|
||||
},
|
||||
Name: "my-chart",
|
||||
Revision: "1.1.0",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test-description", chartDetails.Description)
|
||||
assert.Equal(t, "test-home", chartDetails.Home)
|
||||
assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testEnvironment: 'jsdom',
|
||||
reporters: ['default', 'jest-junit'],
|
||||
collectCoverage: true,
|
||||
transformIgnorePatterns: ['node_modules/(?!(argo-ui)/)'],
|
||||
globals: {
|
||||
'self': {},
|
||||
'window': {localStorage: { getItem: () => '{}', setItem: () => null }},
|
||||
'ts-jest': {
|
||||
isolatedModules: true,
|
||||
},
|
||||
@@ -17,20 +16,3 @@ module.exports = {
|
||||
'.+\\.(css|styl|less|sass|scss)$': 'jest-transform-css',
|
||||
},
|
||||
};
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: (key) => store[key],
|
||||
setItem: (key, value) => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
removeItem: (key) => {
|
||||
delete store[key];
|
||||
}
|
||||
};
|
||||
})();
|
||||
global.localStorage = localStorageMock;
|
||||
@@ -8,16 +8,16 @@ $header: 120px;
|
||||
.application-details {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
&__status-panel {
|
||||
position: fixed;
|
||||
left: $sidebar-width;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
@media screen and (max-width: map-get($breakpoints, xlarge)) {
|
||||
top: 150px;
|
||||
}
|
||||
@media screen and (max-width: map-get($breakpoints, large)) {
|
||||
top: 146px;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 2 * $top-bar-height);
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (max-width: map-get($breakpoints, xxlarge)) {
|
||||
height: calc(100vh - 3 * $top-bar-height);
|
||||
margin-top: $top-bar-height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,11 @@ $header: 120px;
|
||||
|
||||
&__tree {
|
||||
padding: 1em;
|
||||
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
margin-top: 150px;
|
||||
height: calc(100vh - 2 * 70px - 115px);
|
||||
@media screen and (max-width: map-get($breakpoints, xlarge)) {
|
||||
margin-top: 165px;
|
||||
}
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
&__sliding-panel-pagination-wrap {
|
||||
|
||||
@@ -416,125 +416,20 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
|
||||
</React.Fragment>
|
||||
)
|
||||
}}>
|
||||
<div className='application-details__status-panel'>
|
||||
<ApplicationStatusPanel
|
||||
application={application}
|
||||
showDiff={() => this.selectNode(appFullName, 0, 'diff')}
|
||||
showOperation={() => this.setOperationStatusVisible(true)}
|
||||
showConditions={() => this.setConditionsStatusVisible(true)}
|
||||
showMetadataInfo={revision => this.setState({...this.state, revision})}
|
||||
/>
|
||||
</div>
|
||||
<div className='application-details__tree'>
|
||||
{refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
|
||||
{((pref.view === 'tree' || pref.view === 'network') && (
|
||||
<>
|
||||
<DataLoader load={() => services.viewPreferences.getPreferences()}>
|
||||
{viewPref => (
|
||||
<ApplicationDetailsFilters
|
||||
pref={pref}
|
||||
tree={tree}
|
||||
onSetFilter={setFilter}
|
||||
onClearFilter={clearFilter}
|
||||
collapsed={viewPref.hideSidebar}
|
||||
resourceNodes={this.state.filteredGraph}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
<div className='graph-options-panel'>
|
||||
<a
|
||||
className={`group-nodes-button`}
|
||||
onClick={() => {
|
||||
toggleNameDirection();
|
||||
}}
|
||||
title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
|
||||
<i
|
||||
className={classNames({
|
||||
'fa fa-align-right': this.state.truncateNameOnRight,
|
||||
'fa fa-align-left': !this.state.truncateNameOnRight
|
||||
})}
|
||||
/>
|
||||
</a>
|
||||
{(pref.view === 'tree' || pref.view === 'network') && (
|
||||
<Tooltip
|
||||
content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
|
||||
visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
|
||||
duration={showToolTip?.duration}
|
||||
zIndex={1}>
|
||||
<a
|
||||
className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
|
||||
title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
|
||||
onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
|
||||
<i className={classNames('fa fa-object-group fa-fw')} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<span className={`separator`} />
|
||||
<a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
|
||||
<i className='fa fa-plus fa-fw' />
|
||||
</a>
|
||||
<a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
|
||||
<i className='fa fa-minus fa-fw' />
|
||||
</a>
|
||||
<span className={`separator`} />
|
||||
<span>
|
||||
<a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
|
||||
<i className='fa fa-search-plus fa-fw' />
|
||||
</a>
|
||||
<a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
|
||||
<i className='fa fa-search-minus fa-fw' />
|
||||
</a>
|
||||
<div className={`zoom-value`}>{zoomNum}%</div>
|
||||
</span>
|
||||
</div>
|
||||
<ApplicationResourceTree
|
||||
nodeFilter={node => this.filterTreeNode(node, treeFilter)}
|
||||
selectedNodeFullName={this.selectedNodeKey}
|
||||
onNodeClick={fullName => this.selectNode(fullName)}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
|
||||
this.getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
showCompactNodes={pref.groupNodes}
|
||||
userMsgs={pref.userHelpTipMsgs}
|
||||
tree={tree}
|
||||
app={application}
|
||||
showOrphanedResources={pref.orphanedResources}
|
||||
useNetworkingHierarchy={pref.view === 'network'}
|
||||
onClearFilter={clearFilter}
|
||||
onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
|
||||
zoom={pref.zoom}
|
||||
podGroupCount={pref.podGroupCount}
|
||||
appContext={this.appContext}
|
||||
nameDirection={this.state.truncateNameOnRight}
|
||||
filters={pref.resourceFilter}
|
||||
setTreeFilterGraph={setFilterGraph}
|
||||
updateUsrHelpTipMsgs={updateHelpTipState}
|
||||
setShowCompactNodes={setShowCompactNodes}
|
||||
setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
|
||||
getNodeExpansion={node => this.getNodeExpansion(node)}
|
||||
/>
|
||||
</>
|
||||
)) ||
|
||||
(pref.view === 'pods' && (
|
||||
<PodView
|
||||
tree={tree}
|
||||
app={application}
|
||||
onItemClick={fullName => this.selectNode(fullName)}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
|
||||
this.getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
|
||||
/>
|
||||
)) ||
|
||||
(this.state.extensionsMap[pref.view] != null && (
|
||||
<ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
|
||||
)) || (
|
||||
<div>
|
||||
<div className='application-details__wrapper'>
|
||||
<div className='application-details__status-panel'>
|
||||
<ApplicationStatusPanel
|
||||
application={application}
|
||||
showDiff={() => this.selectNode(appFullName, 0, 'diff')}
|
||||
showOperation={() => this.setOperationStatusVisible(true)}
|
||||
showConditions={() => this.setConditionsStatusVisible(true)}
|
||||
showMetadataInfo={revision => this.setState({...this.state, revision})}
|
||||
/>
|
||||
</div>
|
||||
<div className='application-details__tree'>
|
||||
{refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
|
||||
{((pref.view === 'tree' || pref.view === 'network') && (
|
||||
<>
|
||||
<DataLoader load={() => services.viewPreferences.getPreferences()}>
|
||||
{viewPref => (
|
||||
<ApplicationDetailsFilters
|
||||
@@ -543,42 +438,149 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
|
||||
onSetFilter={setFilter}
|
||||
onClearFilter={clearFilter}
|
||||
collapsed={viewPref.hideSidebar}
|
||||
resourceNodes={filteredRes}
|
||||
resourceNodes={this.state.filteredGraph}
|
||||
/>
|
||||
)}
|
||||
</DataLoader>
|
||||
{(filteredRes.length > 0 && (
|
||||
<Paginate
|
||||
page={this.state.page}
|
||||
data={filteredRes}
|
||||
onPageChange={page => this.setState({page})}
|
||||
preferencesKey='application-details'>
|
||||
{data => (
|
||||
<ApplicationResourceList
|
||||
onNodeClick={fullName => this.selectNode(fullName)}
|
||||
resources={data}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(
|
||||
{...node, root: node},
|
||||
application,
|
||||
tree,
|
||||
this.appContext.apis,
|
||||
this.appChanged,
|
||||
() => this.getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
<div className='graph-options-panel'>
|
||||
<a
|
||||
className={`group-nodes-button`}
|
||||
onClick={() => {
|
||||
toggleNameDirection();
|
||||
}}
|
||||
title={this.state.truncateNameOnRight ? 'Truncate resource name right' : 'Truncate resource name left'}>
|
||||
<i
|
||||
className={classNames({
|
||||
'fa fa-align-right': this.state.truncateNameOnRight,
|
||||
'fa fa-align-left': !this.state.truncateNameOnRight
|
||||
})}
|
||||
/>
|
||||
</a>
|
||||
{(pref.view === 'tree' || pref.view === 'network') && (
|
||||
<Tooltip
|
||||
content={AppUtils.userMsgsList[showToolTip?.msgKey] || 'Group Nodes'}
|
||||
visible={pref.groupNodes && showToolTip !== undefined && !showToolTip?.display}
|
||||
duration={showToolTip?.duration}
|
||||
zIndex={1}>
|
||||
<a
|
||||
className={`group-nodes-button group-nodes-button${!pref.groupNodes ? '' : '-on'}`}
|
||||
title={pref.view === 'tree' ? 'Group Nodes' : 'Collapse Pods'}
|
||||
onClick={() => this.toggleCompactView(application.metadata.name, pref)}>
|
||||
<i className={classNames('fa fa-object-group fa-fw')} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<span className={`separator`} />
|
||||
<a className={`group-nodes-button`} onClick={() => expandAll()} title='Expand all child nodes of all parent nodes'>
|
||||
<i className='fa fa-plus fa-fw' />
|
||||
</a>
|
||||
<a className={`group-nodes-button`} onClick={() => collapseAll()} title='Collapse all child nodes of all parent nodes'>
|
||||
<i className='fa fa-minus fa-fw' />
|
||||
</a>
|
||||
<span className={`separator`} />
|
||||
<span>
|
||||
<a className={`group-nodes-button`} onClick={() => setZoom(0.1)} title='Zoom in'>
|
||||
<i className='fa fa-search-plus fa-fw' />
|
||||
</a>
|
||||
<a className={`group-nodes-button`} onClick={() => setZoom(-0.1)} title='Zoom out'>
|
||||
<i className='fa fa-search-minus fa-fw' />
|
||||
</a>
|
||||
<div className={`zoom-value`}>{zoomNum}%</div>
|
||||
</span>
|
||||
</div>
|
||||
<ApplicationResourceTree
|
||||
nodeFilter={node => this.filterTreeNode(node, treeFilter)}
|
||||
selectedNodeFullName={this.selectedNodeKey}
|
||||
onNodeClick={fullName => this.selectNode(fullName)}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
|
||||
this.getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
showCompactNodes={pref.groupNodes}
|
||||
userMsgs={pref.userHelpTipMsgs}
|
||||
tree={tree}
|
||||
app={application}
|
||||
showOrphanedResources={pref.orphanedResources}
|
||||
useNetworkingHierarchy={pref.view === 'network'}
|
||||
onClearFilter={clearFilter}
|
||||
onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)}
|
||||
zoom={pref.zoom}
|
||||
podGroupCount={pref.podGroupCount}
|
||||
appContext={this.appContext}
|
||||
nameDirection={this.state.truncateNameOnRight}
|
||||
filters={pref.resourceFilter}
|
||||
setTreeFilterGraph={setFilterGraph}
|
||||
updateUsrHelpTipMsgs={updateHelpTipState}
|
||||
setShowCompactNodes={setShowCompactNodes}
|
||||
setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)}
|
||||
getNodeExpansion={node => this.getNodeExpansion(node)}
|
||||
/>
|
||||
</>
|
||||
)) ||
|
||||
(pref.view === 'pods' && (
|
||||
<PodView
|
||||
tree={tree}
|
||||
app={application}
|
||||
onItemClick={fullName => this.selectNode(fullName)}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () =>
|
||||
this.getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)}
|
||||
/>
|
||||
)) ||
|
||||
(this.state.extensionsMap[pref.view] != null && (
|
||||
<ExtensionView extension={this.state.extensionsMap[pref.view]} application={application} tree={tree} />
|
||||
)) || (
|
||||
<div>
|
||||
<DataLoader load={() => services.viewPreferences.getPreferences()}>
|
||||
{viewPref => (
|
||||
<ApplicationDetailsFilters
|
||||
pref={pref}
|
||||
tree={tree}
|
||||
onSetFilter={setFilter}
|
||||
onClearFilter={clearFilter}
|
||||
collapsed={viewPref.hideSidebar}
|
||||
resourceNodes={filteredRes}
|
||||
/>
|
||||
)}
|
||||
</Paginate>
|
||||
)) || (
|
||||
<EmptyState icon='fa fa-search'>
|
||||
<h4>No resources found</h4>
|
||||
<h5>Try to change filter criteria</h5>
|
||||
</EmptyState>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DataLoader>
|
||||
{(filteredRes.length > 0 && (
|
||||
<Paginate
|
||||
page={this.state.page}
|
||||
data={filteredRes}
|
||||
onPageChange={page => this.setState({page})}
|
||||
preferencesKey='application-details'>
|
||||
{data => (
|
||||
<ApplicationResourceList
|
||||
onNodeClick={fullName => this.selectNode(fullName)}
|
||||
resources={data}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(
|
||||
{...node, root: node},
|
||||
application,
|
||||
tree,
|
||||
this.appContext.apis,
|
||||
this.appChanged,
|
||||
() => this.getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
tree={tree}
|
||||
/>
|
||||
)}
|
||||
</Paginate>
|
||||
)) || (
|
||||
<EmptyState icon='fa fa-search'>
|
||||
<h4>No resources found</h4>
|
||||
<h5>Try to change filter criteria</h5>
|
||||
</EmptyState>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SlidingPanel isShown={this.state.groupedResources.length > 0} onClose={() => this.closeGroupedNodesPanel()}>
|
||||
<div className='application-details__sliding-panel-pagination-wrap'>
|
||||
@@ -748,12 +750,12 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
|
||||
return [
|
||||
{
|
||||
iconClassName: 'fa fa-info-circle',
|
||||
title: <ActionMenuItem actionLabel='App Details' />,
|
||||
title: <ActionMenuItem actionLabel='Details' />,
|
||||
action: () => this.selectNode(fullName)
|
||||
},
|
||||
{
|
||||
iconClassName: 'fa fa-file-medical',
|
||||
title: <ActionMenuItem actionLabel='App Diff' />,
|
||||
title: <ActionMenuItem actionLabel='Diff' />,
|
||||
action: () => this.selectNode(fullName, 0, 'diff'),
|
||||
disabled: app.status.sync.status === appModels.SyncStatuses.Synced
|
||||
},
|
||||
|
||||
@@ -14,14 +14,21 @@ export interface LayoutProps {
|
||||
|
||||
const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f');
|
||||
|
||||
export const Layout = (props: LayoutProps) => (
|
||||
<div className={props.pref.theme ? 'theme-' + props.pref.theme : 'theme-light'}>
|
||||
<div className={`cd-layout ${props.isExtension ? 'cd-layout--extension' : ''}`}>
|
||||
<Sidebar onVersionClick={props.onVersionClick} navItems={props.navItems} pref={props.pref} />
|
||||
{props.pref.theme ? (document.body.style.background = getBGColor(props.pref.theme)) : null}
|
||||
<div className={`cd-layout__content ${props.pref.hideSidebar ? 'cd-layout__content--sb-collapsed' : 'cd-layout__content--sb-expanded'} custom-styles`}>
|
||||
{props.children}
|
||||
export const Layout = (props: LayoutProps) => {
|
||||
React.useEffect(() => {
|
||||
if (props.pref.theme) {
|
||||
document.body.style.background = getBGColor(props.pref.theme);
|
||||
}
|
||||
}, [props.pref.theme]);
|
||||
|
||||
return (
|
||||
<div className={props.pref.theme ? 'theme-' + props.pref.theme : 'theme-light'}>
|
||||
<div className={`cd-layout ${props.isExtension ? 'cd-layout--extension' : ''}`}>
|
||||
<Sidebar onVersionClick={props.onVersionClick} navItems={props.navItems} pref={props.pref} />
|
||||
<div className={`cd-layout__content ${props.pref.hideSidebar ? 'cd-layout__content--sb-collapsed' : 'cd-layout__content--sb-expanded'} custom-styles`}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,10 +75,10 @@
|
||||
}
|
||||
|
||||
.sb-page-wrapper {
|
||||
padding-left: $sidebar-width - 60px;
|
||||
padding-left: $sidebar-width;
|
||||
|
||||
&__sidebar-collapsed {
|
||||
padding-left: $collapsed-sidebar-width - 60px;
|
||||
padding-left: $collapsed-sidebar-width;
|
||||
.flex-top-bar {
|
||||
left: $collapsed-sidebar-width;
|
||||
}
|
||||
|
||||
65
util/cache/mocks/cacheclient.go
vendored
Normal file
65
util/cache/mocks/cacheclient.go
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
cache "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockCacheClient struct {
|
||||
mock.Mock
|
||||
BaseCache cache.CacheClient
|
||||
ReadDelay time.Duration
|
||||
WriteDelay time.Duration
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) Set(item *cache.Item) error {
|
||||
args := c.Called(item)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
if c.WriteDelay > 0 {
|
||||
time.Sleep(c.WriteDelay)
|
||||
}
|
||||
return c.BaseCache.Set(item)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) Get(key string, obj interface{}) error {
|
||||
args := c.Called(key, obj)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
if c.ReadDelay > 0 {
|
||||
time.Sleep(c.ReadDelay)
|
||||
}
|
||||
return c.BaseCache.Get(key, obj)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) Delete(key string) error {
|
||||
args := c.Called(key)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
if c.WriteDelay > 0 {
|
||||
time.Sleep(c.WriteDelay)
|
||||
}
|
||||
return c.BaseCache.Delete(key)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) OnUpdated(ctx context.Context, key string, callback func() error) error {
|
||||
args := c.Called(ctx, key, callback)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
return c.BaseCache.OnUpdated(ctx, key, callback)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) NotifyUpdated(key string) error {
|
||||
args := c.Called(key)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
return c.BaseCache.NotifyUpdated(key)
|
||||
}
|
||||
Reference in New Issue
Block a user