mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-04-10 02:38:46 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231e359c22 | ||
|
|
2a22e19e06 | ||
|
|
17b0df1168 | ||
|
|
4df9a46e72 | ||
|
|
7113616085 | ||
|
|
c74355d838 | ||
|
|
4629a2b033 | ||
|
|
3983cc4b9d | ||
|
|
951656f083 | ||
|
|
02f7c231b5 | ||
|
|
6f2274a6a1 | ||
|
|
36f80e5f5c | ||
|
|
2b45cc8478 | ||
|
|
152565a32e | ||
|
|
3ef5ba76a9 | ||
|
|
104fb13769 |
8
.github/workflows/image-reuse.yaml
vendored
8
.github/workflows/image-reuse.yaml
vendored
@@ -135,6 +135,14 @@ jobs:
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
echo "GIT_TREE_STATE=$(if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)" >> $GITHUB_ENV
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@4d9e71b726748f254fe64fa44d273194bd18ec91
|
||||
with:
|
||||
large-packages: false
|
||||
docker-images: false
|
||||
swap-storage: false
|
||||
tool-cache: false
|
||||
|
||||
- name: Build and push container image
|
||||
id: image
|
||||
uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 #v4.1.1
|
||||
|
||||
@@ -168,6 +168,11 @@ func NewCommand() *cobra.Command {
|
||||
baseHRef = rootPath
|
||||
}
|
||||
|
||||
var contentTypesList []string
|
||||
if contentTypes != "" {
|
||||
contentTypesList = strings.Split(contentTypes, ";")
|
||||
}
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
ListenPort: listenPort,
|
||||
@@ -183,7 +188,7 @@ func NewCommand() *cobra.Command {
|
||||
DexServerAddr: dexServerAddress,
|
||||
DexTLSConfig: dexTlsConfig,
|
||||
DisableAuth: disableAuth,
|
||||
ContentTypes: strings.Split(contentTypes, ";"),
|
||||
ContentTypes: contentTypesList,
|
||||
EnableGZip: enableGZip,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
@@ -231,7 +236,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", env.StringFromEnv("ARGOCD_SERVER_REPO_SERVER", common.DefaultRepoServerAddr), "Repo server address")
|
||||
command.Flags().StringVar(&dexServerAddress, "dex-server", env.StringFromEnv("ARGOCD_SERVER_DEX_SERVER", common.DefaultDexServerAddr), "Dex server address")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", env.ParseBoolFromEnv("ARGOCD_SERVER_DISABLE_AUTH", false), "Disable client authentication")
|
||||
command.Flags().StringVar(&contentTypes, "api-content-types", "application/json", "Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty.")
|
||||
command.Flags().StringVar(&contentTypes, "api-content-types", env.StringFromEnv("ARGOCD_API_CONTENT_TYPES", "application/json", env.StringFromEnvOpts{AllowEmpty: true}), "Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty.")
|
||||
command.Flags().BoolVar(&enableGZip, "enable-gzip", env.ParseBoolFromEnv("ARGOCD_SERVER_ENABLE_GZIP", true), "Enable GZIP compression")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.Flags().StringVar(&listenHost, "address", env.StringFromEnv("ARGOCD_SERVER_LISTEN_ADDRESS", common.DefaultAddressAPIServer), "Listen on given address")
|
||||
|
||||
@@ -72,6 +72,9 @@ data:
|
||||
server.rootpath: ""
|
||||
# Directory path that contains additional static assets
|
||||
server.staticassets: "/shared/app"
|
||||
# Semicolon-separated list of content types allowed on non-GET requests. Set an empty string to allow all. Be aware
|
||||
# that allowing content types besides application/json may make your API more vulnerable to CSRF attacks.
|
||||
server.api.content.types: "application/json"
|
||||
|
||||
# Set the logging format. One of: text|json (default "text")
|
||||
server.log.format: "text"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -217,7 +217,7 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
|
||||
@@ -5,7 +5,7 @@ kind: Kustomization
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.8.8
|
||||
newTag: v2.8.13
|
||||
resources:
|
||||
- ./application-controller
|
||||
- ./dex
|
||||
|
||||
@@ -25,136 +25,136 @@ spec:
|
||||
env:
|
||||
- name: ARGOCD_SERVER_INSECURE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.insecure
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.insecure
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_BASEHREF
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.basehref
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.basehref
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_ROOTPATH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.rootpath
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.rootpath
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LOGFORMAT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.format
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.format
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LOG_LEVEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.level
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.level
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: repo.server
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: repo.server
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DEX_SERVER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DISABLE_AUTH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.disable.auth
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.disable.auth
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_ENABLE_GZIP
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.enable.gzip
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.enable.gzip
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.timeout.seconds
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.timeout.seconds
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_X_FRAME_OPTIONS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.x.frame.options
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.x.frame.options
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.content.security.policy
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.content.security.policy
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.plaintext
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.plaintext
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER_STRICT_TLS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.strict.tls
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.strict.tls
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DEX_SERVER_PLAINTEXT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.plaintext
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.plaintext
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DEX_SERVER_STRICT_TLS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.strict.tls
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.strict.tls
|
||||
optional: true
|
||||
- name: ARGOCD_TLS_MIN_VERSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.minversion
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.minversion
|
||||
optional: true
|
||||
- name: ARGOCD_TLS_MAX_VERSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.maxversion
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.maxversion
|
||||
optional: true
|
||||
- name: ARGOCD_TLS_CIPHERS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.ciphers
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.ciphers
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_CONNECTION_STATUS_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.connection.status.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.connection.status.cache.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OIDC_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.oidc.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.oidc.cache.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LOGIN_ATTEMPTS_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.login.attempts.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.login.attempts.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_STATIC_ASSETS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -163,16 +163,16 @@ spec:
|
||||
optional: true
|
||||
- name: ARGOCD_APP_STATE_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.app.state.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.app.state.cache.expiration
|
||||
optional: true
|
||||
- name: REDIS_SERVER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.server
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.server
|
||||
optional: true
|
||||
- name: REDIS_COMPRESSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -181,52 +181,58 @@ spec:
|
||||
optional: true
|
||||
- name: REDISDB
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.db
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.db
|
||||
optional: true
|
||||
- name: ARGOCD_DEFAULT_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.default.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.default.cache.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_MAX_COOKIE_NUMBER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.http.cookie.maxnumber
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.http.cookie.maxnumber
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LISTEN_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.listen.address
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.listen.address
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_METRICS_LISTEN_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.metrics.listen.address
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.metrics.listen.address
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.address
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.address
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: application.namespaces
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: application.namespaces
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_ENABLE_PROXY_EXTENSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.enable.proxy.extension
|
||||
optional: true
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.api.content.types
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: ssh-known-hosts
|
||||
mountPath: /app/config/ssh
|
||||
|
||||
@@ -18880,7 +18880,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -19180,7 +19180,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -19232,7 +19232,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -19451,7 +19451,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
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.8.8
|
||||
newTag: v2.8.13
|
||||
|
||||
@@ -12,7 +12,7 @@ patches:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.8.8
|
||||
newTag: v2.8.13
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/applicationset-controller
|
||||
|
||||
@@ -20129,7 +20129,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -20252,7 +20252,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -20328,7 +20328,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -20654,7 +20654,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -20706,7 +20706,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -20995,7 +20995,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -21241,7 +21247,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -1635,7 +1635,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -1758,7 +1758,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -1834,7 +1834,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -2160,7 +2160,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -2212,7 +2212,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -2501,7 +2501,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -2747,7 +2753,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -19230,7 +19230,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -19353,7 +19353,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -19429,7 +19429,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -19711,7 +19711,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -19763,7 +19763,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -20050,7 +20050,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -20296,7 +20302,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -736,7 +736,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -859,7 +859,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -935,7 +935,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1217,7 +1217,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -1269,7 +1269,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -1556,7 +1556,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -1802,7 +1808,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.8.8
|
||||
image: quay.io/argoproj/argocd:v2.8.13
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -321,6 +321,15 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq
|
||||
return nil, security.NamespaceNotPermittedError(appNs)
|
||||
}
|
||||
|
||||
// Don't let the app creator set the operation explicitly. Those requests should always go through the Sync API.
|
||||
if a.Operation != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"application": a.Name,
|
||||
argocommon.SecurityField: argocommon.SecurityLow,
|
||||
}).Warn("User attempted to set operation on application creation. This could have allowed them to bypass branch protection rules by setting manifests directly. Ignoring the set operation.")
|
||||
a.Operation = nil
|
||||
}
|
||||
|
||||
created, err := s.appclientset.ArgoprojV1alpha1().Applications(appNs).Create(ctx, a, metav1.CreateOptions{})
|
||||
if err == nil {
|
||||
s.logAppEvent(created, ctx, argo.EventReasonResourceCreated, "created application")
|
||||
|
||||
@@ -1438,6 +1438,27 @@ func TestCreateAppWithDestName(t *testing.T) {
|
||||
assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.com")
|
||||
}
|
||||
|
||||
// TestCreateAppWithOperation tests that an application created with an operation is created with the operation removed.
|
||||
// Avoids regressions of https://github.com/argoproj/argo-cd/security/advisories/GHSA-g623-jcgg-mhmm
|
||||
func TestCreateAppWithOperation(t *testing.T) {
|
||||
appServer := newTestAppServer(t)
|
||||
testApp := newTestAppWithDestName()
|
||||
testApp.Operation = &appsv1.Operation{
|
||||
Sync: &appsv1.SyncOperation{
|
||||
Manifests: []string{
|
||||
"test",
|
||||
},
|
||||
},
|
||||
}
|
||||
createReq := application.ApplicationCreateRequest{
|
||||
Application: testApp,
|
||||
}
|
||||
app, err := appServer.Create(context.Background(), &createReq)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, app)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
func TestUpdateApp(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
appServer := newTestAppServer(t, testApp)
|
||||
|
||||
@@ -38,12 +38,12 @@ type terminalHandler struct {
|
||||
allowedShells []string
|
||||
namespace string
|
||||
enabledNamespaces []string
|
||||
sessionManager util_session.SessionManager
|
||||
sessionManager *util_session.SessionManager
|
||||
}
|
||||
|
||||
// NewHandler returns a new terminal handler.
|
||||
func NewHandler(appLister applisters.ApplicationLister, namespace string, enabledNamespaces []string, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache,
|
||||
appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager util_session.SessionManager) *terminalHandler {
|
||||
appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager *util_session.SessionManager) *terminalHandler {
|
||||
return &terminalHandler{
|
||||
appLister: appLister,
|
||||
db: db,
|
||||
|
||||
@@ -37,7 +37,7 @@ type terminalSession struct {
|
||||
tty bool
|
||||
readLock sync.Mutex
|
||||
writeLock sync.Mutex
|
||||
sessionManager util_session.SessionManager
|
||||
sessionManager *util_session.SessionManager
|
||||
token *string
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func getToken(r *http.Request) (string, error) {
|
||||
}
|
||||
|
||||
// newTerminalSession create terminalSession
|
||||
func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager util_session.SessionManager) (*terminalSession, error) {
|
||||
func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) {
|
||||
token, err := getToken(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -977,10 +977,12 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
|
||||
}
|
||||
if len(a.ContentTypes) > 0 {
|
||||
handler = enforceContentTypes(handler, a.ContentTypes)
|
||||
} else {
|
||||
log.WithField(common.SecurityField, common.SecurityHigh).Warnf("Content-Type enforcement is disabled, which may make your API vulnerable to CSRF attacks")
|
||||
}
|
||||
mux.Handle("/api/", handler)
|
||||
|
||||
terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr).
|
||||
terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr).
|
||||
WithFeatureFlagMiddleware(a.settingsMgr.GetSettings)
|
||||
th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal)
|
||||
mux.Handle("/terminal", th)
|
||||
|
||||
@@ -1349,3 +1349,46 @@ func TestReplaceBaseHRef(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_enforceContentTypes(t *testing.T) {
|
||||
getBaseHandler := func(t *testing.T, allow bool) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
assert.True(t, allow, "http handler was hit when it should have been blocked by content type enforcement")
|
||||
writer.WriteHeader(200)
|
||||
})
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
t.Run("GET - not providing a content type, should still succeed", func(t *testing.T) {
|
||||
handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc)
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("POST", func(t *testing.T) {
|
||||
handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc)
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, 415, resp.StatusCode, "didn't provide a content type, should have gotten an error")
|
||||
|
||||
req = httptest.NewRequest("POST", "/", nil)
|
||||
req.Header = map[string][]string{"Content-Type": {"application/json"}}
|
||||
w = httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp = w.Result()
|
||||
assert.Equal(t, 200, resp.StatusCode, "should have passed, since an allowed content type was provided")
|
||||
|
||||
req = httptest.NewRequest("POST", "/", nil)
|
||||
req.Header = map[string][]string{"Content-Type": {"not-allowed"}}
|
||||
w = httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp = w.Result()
|
||||
assert.Equal(t, 415, resp.StatusCode, "should not have passed, since a disallowed content type was provided")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
RevisionHelpIcon
|
||||
} from '../../../shared/components';
|
||||
import {BadgePanel, Spinner} from '../../../shared/components';
|
||||
import {Consumer, ContextApis} from '../../../shared/context';
|
||||
import {AuthSettingsCtx, Consumer, ContextApis} from '../../../shared/context';
|
||||
import * as models from '../../../shared/models';
|
||||
import {services} from '../../../shared/services';
|
||||
|
||||
@@ -30,6 +30,7 @@ import {EditAnnotations} from './edit-annotations';
|
||||
|
||||
import './application-summary.scss';
|
||||
import {DeepLinks} from '../../../shared/components/deep-links';
|
||||
import {ExternalLinks} from '../application-urls';
|
||||
|
||||
function swap(array: any[], a: number, b: number) {
|
||||
array = array.slice();
|
||||
@@ -47,6 +48,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
const source = getAppDefaultSource(app);
|
||||
const isHelm = source.hasOwnProperty('chart');
|
||||
const initialState = app.spec.destination.server === undefined ? 'NAME' : 'URL';
|
||||
const useAuthSettingsCtx = React.useContext(AuthSettingsCtx);
|
||||
const [destFormat, setDestFormat] = React.useState(initialState);
|
||||
const [changeSync, setChangeSync] = React.useState(false);
|
||||
|
||||
@@ -325,20 +327,19 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const urls = app.status.summary.externalURLs || [];
|
||||
const urls = ExternalLinks(app.status.summary.externalURLs);
|
||||
if (urls.length > 0) {
|
||||
attributes.push({
|
||||
title: 'URLs',
|
||||
view: (
|
||||
<React.Fragment>
|
||||
{urls
|
||||
.map(item => item.split('|'))
|
||||
.map((parts, i) => (
|
||||
<a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='__blank'>
|
||||
{parts[0]}
|
||||
{urls.map((url, i) => {
|
||||
return (
|
||||
<a key={i} href={url.ref} target='__blank'>
|
||||
{url.title}
|
||||
</a>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
});
|
||||
@@ -589,7 +590,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
</div>
|
||||
)}
|
||||
</Consumer>
|
||||
<BadgePanel app={props.app.metadata.name} />
|
||||
<BadgePanel app={props.app.metadata.name} appNamespace={props.app.metadata.namespace} nsEnabled={useAuthSettingsCtx?.appsInAnyNamespaceEnabled} />
|
||||
<EditablePanel
|
||||
save={updateApp}
|
||||
values={app}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ExternalLink, InvalidExternalLinkError} from './application-urls';
|
||||
import { ExternalLink, ExternalLinks, InvalidExternalLinkError } from './application-urls';
|
||||
|
||||
test('rejects malicious URLs', () => {
|
||||
expect(() => {
|
||||
@@ -7,6 +7,16 @@ test('rejects malicious URLs', () => {
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('data:text/html;<h1>hi</h1>');
|
||||
}).toThrowError(InvalidExternalLinkError);
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('title|data:text/html;<h1>hi</h1>');
|
||||
}).toThrowError(InvalidExternalLinkError);
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('data:title|data:text/html;<h1>hi</h1>');
|
||||
}).toThrowError(InvalidExternalLinkError);
|
||||
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('data:title|https://localhost:8080/applications');
|
||||
}).not.toThrowError(InvalidExternalLinkError);
|
||||
});
|
||||
|
||||
test('allows absolute URLs', () => {
|
||||
@@ -18,3 +28,59 @@ test('allows relative URLs', () => {
|
||||
window.location = new URL('https://localhost:8080/applications');
|
||||
expect(new ExternalLink('/applications').ref).toEqual('/applications');
|
||||
});
|
||||
|
||||
|
||||
test('URLs format', () => {
|
||||
expect(new ExternalLink('https://localhost:8080/applications')).toEqual({
|
||||
ref: 'https://localhost:8080/applications',
|
||||
title: 'https://localhost:8080/applications',
|
||||
})
|
||||
expect(new ExternalLink('title|https://localhost:8080/applications')).toEqual({
|
||||
ref: 'https://localhost:8080/applications',
|
||||
title: 'title',
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
test('malicious URLs from list to be removed', () => {
|
||||
const urls: string[] = [
|
||||
'javascript:alert("hi")',
|
||||
'https://localhost:8080/applications',
|
||||
]
|
||||
const links = ExternalLinks(urls);
|
||||
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links).toContainEqual({
|
||||
ref: 'https://localhost:8080/applications',
|
||||
title: 'https://localhost:8080/applications',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('list to be sorted', () => {
|
||||
const urls: string[] = [
|
||||
'https://a',
|
||||
'https://b',
|
||||
'a|https://c',
|
||||
'z|https://c',
|
||||
'x|https://d',
|
||||
'x|https://c',
|
||||
]
|
||||
const links = ExternalLinks(urls);
|
||||
|
||||
// 'a|https://c',
|
||||
// 'x|https://c',
|
||||
// 'x|https://d',
|
||||
// 'z|https://c',
|
||||
// 'https://a',
|
||||
// 'https://b',
|
||||
expect(links).toHaveLength(6);
|
||||
expect(links[0].title).toEqual('a')
|
||||
expect(links[1].title).toEqual('x')
|
||||
expect(links[1].ref).toEqual('https://c')
|
||||
expect(links[2].title).toEqual('x')
|
||||
expect(links[2].ref).toEqual('https://d')
|
||||
expect(links[3].title).toEqual('z')
|
||||
expect(links[4].title).toEqual('https://a')
|
||||
expect(links[5].title).toEqual('https://b')
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ExternalLink {
|
||||
}
|
||||
}
|
||||
|
||||
export const ApplicationURLs = ({urls}: {urls: string[]}) => {
|
||||
export const ExternalLinks = (urls?: string[]) => {
|
||||
const externalLinks: ExternalLink[] = [];
|
||||
for (const url of urls || []) {
|
||||
try {
|
||||
@@ -42,16 +42,26 @@ export const ApplicationURLs = ({urls}: {urls: string[]}) => {
|
||||
|
||||
// sorted alphabetically & links with titles first
|
||||
externalLinks.sort((a, b) => {
|
||||
if (a.title !== '' && b.title !== '') {
|
||||
const hasTitle = (x: ExternalLink): boolean => {
|
||||
return x.title !== x.ref && x.title !== '';
|
||||
};
|
||||
|
||||
if (hasTitle(a) && hasTitle(b) && a.title !== b.title) {
|
||||
return a.title > b.title ? 1 : -1;
|
||||
} else if (a.title === '') {
|
||||
} else if (hasTitle(b) && !hasTitle(a)) {
|
||||
return 1;
|
||||
} else if (b.title === '') {
|
||||
} else if (hasTitle(a) && !hasTitle(b)) {
|
||||
return -1;
|
||||
}
|
||||
return a.ref > b.ref ? 1 : -1;
|
||||
});
|
||||
|
||||
return externalLinks;
|
||||
};
|
||||
|
||||
export const ApplicationURLs = ({urls}: {urls: string[]}) => {
|
||||
const externalLinks: ExternalLink[] = ExternalLinks(urls);
|
||||
|
||||
return (
|
||||
((externalLinks || []).length > 0 && (
|
||||
<div className='applications-list__external-links-icon-container'>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Context} from '../../context';
|
||||
|
||||
require('./badge-panel.scss');
|
||||
|
||||
export const BadgePanel = ({app, project}: {app?: string; project?: string}) => {
|
||||
export const BadgePanel = ({app, project, appNamespace, nsEnabled}: {app?: string; project?: string; appNamespace?: string; nsEnabled?: boolean}) => {
|
||||
const [badgeType, setBadgeType] = React.useState('URL');
|
||||
const context = React.useContext(Context);
|
||||
if (!app && !project) {
|
||||
@@ -20,6 +20,9 @@ export const BadgePanel = ({app, project}: {app?: string; project?: string}) =>
|
||||
let alt = '';
|
||||
if (app) {
|
||||
badgeURL = `${root}api/badge?name=${app}&revision=true`;
|
||||
if (nsEnabled) {
|
||||
badgeURL += `&namespace=${appNamespace}`;
|
||||
}
|
||||
entityURL = `${root}applications/${app}`;
|
||||
alt = 'App Status';
|
||||
} else if (project) {
|
||||
|
||||
@@ -51,19 +51,19 @@ export default {
|
||||
},
|
||||
|
||||
post(url: string) {
|
||||
return initHandlers(agent.post(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.post(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
put(url: string) {
|
||||
return initHandlers(agent.put(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.put(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
patch(url: string) {
|
||||
return initHandlers(agent.patch(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.patch(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
delete(url: string) {
|
||||
return initHandlers(agent.del(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.del(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
loadEventSource(url: string): Observable<string> {
|
||||
|
||||
41
util/cache/redis.go
vendored
41
util/cache/redis.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
ioutil "github.com/argoproj/argo-cd/v2/util/io"
|
||||
@@ -155,41 +156,27 @@ type MetricsRegistry interface {
|
||||
ObserveRedisRequestDuration(duration time.Duration)
|
||||
}
|
||||
|
||||
var metricStartTimeKey = struct{}{}
|
||||
|
||||
type redisHook struct {
|
||||
registry MetricsRegistry
|
||||
}
|
||||
|
||||
func (rh *redisHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
|
||||
return context.WithValue(ctx, metricStartTimeKey, time.Now()), nil
|
||||
func (rh *redisHook) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := next(ctx, network, addr)
|
||||
return conn, err
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *redisHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
|
||||
cmdErr := cmd.Err()
|
||||
rh.registry.IncRedisRequest(cmdErr != nil && cmdErr != redis.Nil)
|
||||
func (rh *redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||
startTime := time.Now()
|
||||
|
||||
startTime := ctx.Value(metricStartTimeKey).(time.Time)
|
||||
duration := time.Since(startTime)
|
||||
rh.registry.ObserveRedisRequestDuration(duration)
|
||||
err := next(ctx, cmd)
|
||||
rh.registry.IncRedisRequest(err != nil && err != redis.Nil)
|
||||
rh.registry.ObserveRedisRequestDuration(time.Since(startTime))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisHook) BeforeProcessPipeline(ctx context.Context, _ []redis.Cmder) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (redisHook) AfterProcessPipeline(_ context.Context, _ []redis.Cmder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisHook) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (redisHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
|
||||
|
||||
40
util/cache/redis_hook.go
vendored
40
util/cache/redis_hook.go
vendored
@@ -2,14 +2,13 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const NoSuchHostErr = "no such host"
|
||||
|
||||
type argoRedisHooks struct {
|
||||
reconnectCallback func()
|
||||
}
|
||||
@@ -18,32 +17,23 @@ func NewArgoRedisHook(reconnectCallback func()) *argoRedisHooks {
|
||||
return &argoRedisHooks{reconnectCallback: reconnectCallback}
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
|
||||
if cmd.Err() != nil && strings.Contains(cmd.Err().Error(), NoSuchHostErr) {
|
||||
log.Warnf("Reconnect to redis because error: \"%v\"", cmd.Err())
|
||||
hook.reconnectCallback()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return nil
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := next(ctx, network, addr)
|
||||
return conn, err
|
||||
}
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return nil
|
||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||
var dnsError *net.DNSError
|
||||
err := next(ctx, cmd)
|
||||
if err != nil && errors.As(err, &dnsError) {
|
||||
log.Warnf("Reconnect to redis because error: \"%v\"", err)
|
||||
hook.reconnectCallback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
|
||||
|
||||
33
util/cache/redis_hook_test.go
vendored
33
util/cache/redis_hook_test.go
vendored
@@ -1,38 +1,53 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func Test_ReconnectCallbackHookCalled(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
called := false
|
||||
hook := NewArgoRedisHook(func() {
|
||||
called = true
|
||||
})
|
||||
|
||||
cmd := &redis.StringCmd{}
|
||||
cmd.SetErr(errors.New("Failed to resync revoked tokens. retrying again in 1 minute: dial tcp: lookup argocd-redis on 10.179.0.10:53: no such host"))
|
||||
|
||||
_ = hook.AfterProcess(context.Background(), cmd)
|
||||
faultyDNSRedisClient := redis.NewClient(&redis.Options{Addr: "invalidredishost.invalid:12345"})
|
||||
faultyDNSRedisClient.AddHook(hook)
|
||||
|
||||
faultyDNSClient := NewRedisCache(faultyDNSRedisClient, 60*time.Second, RedisCompressionNone)
|
||||
err = faultyDNSClient.Set(&Item{Key: "baz", Object: "foo"})
|
||||
assert.Equal(t, called, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_ReconnectCallbackHookNotCalled(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
called := false
|
||||
hook := NewArgoRedisHook(func() {
|
||||
called = true
|
||||
})
|
||||
cmd := &redis.StringCmd{}
|
||||
cmd.SetErr(errors.New("Something wrong"))
|
||||
|
||||
_ = hook.AfterProcess(context.Background(), cmd)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
redisClient.AddHook(hook)
|
||||
client := NewRedisCache(redisClient, 60*time.Second, RedisCompressionNone)
|
||||
|
||||
err = client.Set(&Item{Key: "foo", Object: "bar"})
|
||||
assert.Equal(t, called, false)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
92
util/cache/redis_test.go
vendored
92
util/cache/redis_test.go
vendored
@@ -2,14 +2,59 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
promcm "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
redisRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_redis_request_total",
|
||||
},
|
||||
[]string{"initiator", "failed"},
|
||||
)
|
||||
redisRequestHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_redis_request_duration",
|
||||
Buckets: []float64{0.1, 0.25, .5, 1, 2},
|
||||
},
|
||||
[]string{"initiator"},
|
||||
)
|
||||
)
|
||||
|
||||
type MockMetricsServer struct {
|
||||
registry *prometheus.Registry
|
||||
redisRequestCounter *prometheus.CounterVec
|
||||
redisRequestHistogram *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
func NewMockMetricsServer() *MockMetricsServer {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(redisRequestCounter)
|
||||
registry.MustRegister(redisRequestHistogram)
|
||||
return &MockMetricsServer{
|
||||
registry: registry,
|
||||
redisRequestCounter: redisRequestCounter,
|
||||
redisRequestHistogram: redisRequestHistogram,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockMetricsServer) IncRedisRequest(failed bool) {
|
||||
m.redisRequestCounter.WithLabelValues("mock", strconv.FormatBool(failed)).Inc()
|
||||
}
|
||||
|
||||
func (m *MockMetricsServer) ObserveRedisRequestDuration(duration time.Duration) {
|
||||
m.redisRequestHistogram.WithLabelValues("mock").Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
func TestRedisSetCache(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
@@ -70,3 +115,50 @@ func TestRedisSetCacheCompressed(t *testing.T) {
|
||||
|
||||
assert.Equal(t, testValue, result)
|
||||
}
|
||||
|
||||
func TestRedisMetrics(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
metric := &promcm.Metric{}
|
||||
ms := NewMockMetricsServer()
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
faultyRedisClient := redis.NewClient(&redis.Options{Addr: "invalidredishost.invalid:12345"})
|
||||
CollectMetrics(redisClient, ms)
|
||||
CollectMetrics(faultyRedisClient, ms)
|
||||
|
||||
client := NewRedisCache(redisClient, 60*time.Second, RedisCompressionNone)
|
||||
faultyClient := NewRedisCache(faultyRedisClient, 60*time.Second, RedisCompressionNone)
|
||||
var res string
|
||||
|
||||
//client successful request
|
||||
err = client.Set(&Item{Key: "foo", Object: "bar"})
|
||||
assert.NoError(t, err)
|
||||
err = client.Get("foo", &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
c, err := ms.redisRequestCounter.GetMetricWithLabelValues("mock", "false")
|
||||
assert.NoError(t, err)
|
||||
err = c.Write(metric)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, metric.Counter.GetValue(), float64(2))
|
||||
|
||||
//faulty client failed request
|
||||
err = faultyClient.Get("foo", &res)
|
||||
assert.Error(t, err)
|
||||
c, err = ms.redisRequestCounter.GetMetricWithLabelValues("mock", "true")
|
||||
assert.NoError(t, err)
|
||||
err = c.Write(metric)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, metric.Counter.GetValue(), float64(1))
|
||||
|
||||
//both clients histogram count
|
||||
o, err := ms.redisRequestHistogram.GetMetricWithLabelValues("mock")
|
||||
assert.NoError(t, err)
|
||||
err = o.(prometheus.Metric).Write(metric)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int(metric.Histogram.GetSampleCount()), 3)
|
||||
}
|
||||
|
||||
13
util/env/env.go
vendored
13
util/env/env.go
vendored
@@ -124,8 +124,17 @@ func ParseDurationFromEnv(env string, defaultValue, min, max time.Duration) time
|
||||
return dur
|
||||
}
|
||||
|
||||
func StringFromEnv(env string, defaultValue string) string {
|
||||
if str := os.Getenv(env); str != "" {
|
||||
type StringFromEnvOpts struct {
|
||||
// AllowEmpty allows the value to be empty as long as the environment variable is set.
|
||||
AllowEmpty bool
|
||||
}
|
||||
|
||||
func StringFromEnv(env string, defaultValue string, opts ...StringFromEnvOpts) string {
|
||||
opt := StringFromEnvOpts{}
|
||||
for _, o := range opts {
|
||||
opt.AllowEmpty = opt.AllowEmpty || o.AllowEmpty
|
||||
}
|
||||
if str, ok := os.LookupEnv(env); opt.AllowEmpty && ok || str != "" {
|
||||
return str
|
||||
}
|
||||
return defaultValue
|
||||
|
||||
19
util/env/env_test.go
vendored
19
util/env/env_test.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestParseNumFromEnv(t *testing.T) {
|
||||
@@ -167,19 +168,25 @@ func TestStringFromEnv(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
env string
|
||||
env *string
|
||||
expected string
|
||||
def string
|
||||
opts []StringFromEnvOpts
|
||||
}{
|
||||
{"Some string", "true", "true", def},
|
||||
{"Empty string with default", "", def, def},
|
||||
{"Empty string without default", "", "", ""},
|
||||
{"Some string", pointer.String("true"), "true", def, nil},
|
||||
{"Empty string with default", pointer.String(""), def, def, nil},
|
||||
{"Empty string without default", pointer.String(""), "", "", nil},
|
||||
{"No env variable with default allow empty", nil, "default", "default", []StringFromEnvOpts{{AllowEmpty: true}}},
|
||||
{"Some variable with default allow empty", pointer.String("true"), "true", "default", []StringFromEnvOpts{{AllowEmpty: true}}},
|
||||
{"Empty variable with default allow empty", pointer.String(""), "", "default", []StringFromEnvOpts{{AllowEmpty: true}}},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(envKey, tt.env)
|
||||
b := StringFromEnv(envKey, tt.def)
|
||||
if tt.env != nil {
|
||||
t.Setenv(envKey, *tt.env)
|
||||
}
|
||||
b := StringFromEnv(envKey, tt.def, tt.opts...)
|
||||
assert.Equal(t, tt.expected, b)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
executil "github.com/argoproj/argo-cd/v2/util/exec"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -19,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
executil "github.com/argoproj/argo-cd/v2/util/exec"
|
||||
|
||||
"github.com/argoproj/pkg/sync"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -34,6 +35,8 @@ import (
|
||||
var (
|
||||
globalLock = sync.NewKeyLock()
|
||||
indexLock = sync.NewKeyLock()
|
||||
|
||||
OCINotEnabledErr = errors.New("could not perform the action when oci is not enabled")
|
||||
)
|
||||
|
||||
type Creds struct {
|
||||
@@ -401,6 +404,10 @@ func getIndexURL(rawURL string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) {
|
||||
if !c.enableOci {
|
||||
return nil, OCINotEnabledErr
|
||||
}
|
||||
|
||||
tagsURL := strings.Replace(fmt.Sprintf("%s/%s", c.repoURL, chart), "https://", "", 1)
|
||||
indexLock.Lock(tagsURL)
|
||||
defer indexLock.Unlock(tagsURL)
|
||||
@@ -428,10 +435,12 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
|
||||
TLSClientConfig: tlsConf,
|
||||
DisableKeepAlives: true,
|
||||
}}
|
||||
|
||||
repoHost, _, _ := strings.Cut(tagsURL, "/")
|
||||
repo.Client = &auth.Client{
|
||||
Client: client,
|
||||
Cache: nil,
|
||||
Credential: auth.StaticCredential(c.repoURL, auth.Credential{
|
||||
Credential: auth.StaticCredential(repoHost, auth.Credential{
|
||||
Username: c.creds.Username,
|
||||
Password: c.creds.Password,
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -159,41 +160,129 @@ func TestGetIndexURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTagsFromUrl(t *testing.T) {
|
||||
t.Run("should return tags correctly while following the link header", func(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("called %s", r.URL.Path)
|
||||
responseTags := TagsList{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if !strings.Contains(r.URL.String(), "token") {
|
||||
w.Header().Set("Link", fmt.Sprintf("<https://%s%s?token=next-token>; rel=next", r.Host, r.URL.Path))
|
||||
responseTags.Tags = []string{"first"}
|
||||
} else {
|
||||
responseTags.Tags = []string{
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0_build",
|
||||
"2.8.0-prerelease_build",
|
||||
"2.8.0-prerelease.1_build.1234",
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(responseTags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
|
||||
client := NewClient(server.URL, Creds{InsecureSkipVerify: true}, true, "")
|
||||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
"first",
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
"2.8.0-prerelease+build",
|
||||
"2.8.0-prerelease.1+build.1234",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("should return an error not when oci is not enabled", func(t *testing.T) {
|
||||
client := NewClient("example.com", Creds{}, false, "")
|
||||
|
||||
_, err := client.GetTags("my-chart", true)
|
||||
assert.ErrorIs(t, OCINotEnabledErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("called %s", r.URL.Path)
|
||||
responseTags := TagsList{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if !strings.Contains(r.URL.String(), "token") {
|
||||
w.Header().Set("Link", fmt.Sprintf("<https://%s%s?token=next-token>; rel=next", r.Host, r.URL.Path))
|
||||
responseTags.Tags = []string{"first"}
|
||||
} else {
|
||||
responseTags.Tags = []string{
|
||||
"second",
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="helm repo to get tags"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("authorization received %s", authorization)
|
||||
|
||||
responseTags := TagsList{
|
||||
Tags: []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0_build",
|
||||
"2.8.0-prerelease_build",
|
||||
"2.8.0-prerelease.1_build.1234",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(responseTags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := NewClient(server.URL, Creds{InsecureSkipVerify: true}, true, "")
|
||||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
"first",
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
"2.8.0-prerelease+build",
|
||||
"2.8.0-prerelease.1+build.1234",
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
repoURL string
|
||||
}{
|
||||
{
|
||||
name: "should login correctly when the repo path is in the server root with http scheme",
|
||||
repoURL: server.URL,
|
||||
},
|
||||
{
|
||||
name: "should login correctly when the repo path is not in the server root with http scheme",
|
||||
repoURL: fmt.Sprintf("%s/my-repo", server.URL),
|
||||
},
|
||||
{
|
||||
name: "should login correctly when the repo path is in the server root without http scheme",
|
||||
repoURL: serverURL.Host,
|
||||
},
|
||||
{
|
||||
name: "should login correctly when the repo path is not in the server root without http scheme",
|
||||
repoURL: fmt.Sprintf("%s/my-repo", serverURL.Host),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
client := NewClient(testCase.repoURL, Creds{
|
||||
InsecureSkipVerify: true,
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
}, true, "")
|
||||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
"2.8.0-prerelease+build",
|
||||
"2.8.0-prerelease.1+build.1234",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -41,6 +42,7 @@ type SessionManager struct {
|
||||
storage UserStateStorage
|
||||
sleep func(d time.Duration)
|
||||
verificationDelayNoiseEnabled bool
|
||||
failedLock sync.RWMutex
|
||||
}
|
||||
|
||||
// LoginAttempts is a timestamped counter for failed login attempts
|
||||
@@ -69,7 +71,7 @@ const (
|
||||
// Maximum length of username, too keep the cache's memory signature low
|
||||
maxUsernameLength = 32
|
||||
// The default maximum session cache size
|
||||
defaultMaxCacheSize = 1000
|
||||
defaultMaxCacheSize = 10000
|
||||
// The default number of maximum login failures before delay kicks in
|
||||
defaultMaxLoginFailures = 5
|
||||
// The default time in seconds for the failure window
|
||||
@@ -284,7 +286,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
|
||||
return token.Claims, newToken, nil
|
||||
}
|
||||
|
||||
// GetLoginFailures retrieves the login failure information from the cache
|
||||
// GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner.
|
||||
func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
|
||||
// Get failures from the cache
|
||||
var failures map[string]LoginAttempts
|
||||
@@ -299,25 +301,43 @@ func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
|
||||
return failures
|
||||
}
|
||||
|
||||
func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int {
|
||||
func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int {
|
||||
expiredCount := 0
|
||||
for key, attempt := range *failures {
|
||||
for key, attempt := range failures {
|
||||
if time.Since(attempt.LastFailed) > maxAge*time.Second {
|
||||
expiredCount += 1
|
||||
delete(*failures, key)
|
||||
delete(failures, key)
|
||||
}
|
||||
}
|
||||
return expiredCount
|
||||
}
|
||||
|
||||
// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache.
|
||||
func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
|
||||
idx := rand.Intn(len(failures) - 1)
|
||||
i := 0
|
||||
for key := range failures {
|
||||
if i == idx {
|
||||
if key == common.ArgoCDAdminUsername || key == username {
|
||||
return pickRandomNonAdminLoginFailure(failures, username)
|
||||
}
|
||||
return &key
|
||||
}
|
||||
i++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
|
||||
func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
|
||||
mgr.failedLock.Lock()
|
||||
defer mgr.failedLock.Unlock()
|
||||
|
||||
failures := mgr.GetLoginFailures()
|
||||
|
||||
// Expire old entries in the cache if we have a failure window defined.
|
||||
if window := getLoginFailureWindow(); window > 0 {
|
||||
count := expireOldFailedAttempts(window, &failures)
|
||||
count := expireOldFailedAttempts(window, failures)
|
||||
if count > 0 {
|
||||
log.Infof("Expired %d entries from session cache due to max age reached", count)
|
||||
}
|
||||
@@ -327,23 +347,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
|
||||
// prevent overbloating the cache with fake entries, as this could lead to
|
||||
// memory exhaustion and ultimately in a DoS. We remove a single entry to
|
||||
// replace it with the new one.
|
||||
//
|
||||
// Chances are that we remove the one that is under active attack, but this
|
||||
// chance is low (1:cache_size)
|
||||
if failed && len(failures) >= getMaximumCacheSize() {
|
||||
log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
|
||||
idx := rand.Intn(len(failures) - 1)
|
||||
var rmUser string
|
||||
i := 0
|
||||
for key := range failures {
|
||||
if i == idx {
|
||||
rmUser = key
|
||||
delete(failures, key)
|
||||
break
|
||||
}
|
||||
i++
|
||||
rmUser := pickRandomNonAdminLoginFailure(failures, username)
|
||||
if rmUser != nil {
|
||||
delete(failures, *rmUser)
|
||||
log.Infof("Deleted entry for user %s from cache", *rmUser)
|
||||
}
|
||||
log.Infof("Deleted entry for user %s from cache", rmUser)
|
||||
}
|
||||
|
||||
attempt, ok := failures[username]
|
||||
@@ -374,6 +384,8 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
|
||||
|
||||
// Get the current login failure attempts for given username
|
||||
func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
|
||||
mgr.failedLock.RLock()
|
||||
defer mgr.failedLock.RUnlock()
|
||||
failures := mgr.GetLoginFailures()
|
||||
attempt, ok := failures[username]
|
||||
if !ok {
|
||||
|
||||
@@ -1188,3 +1188,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
||||
assert.ErrorIs(t, err, common.TokenVerificationErr)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
|
||||
t.Run("Not pick admin user from the queue", func(t *testing.T) {
|
||||
failures := map[string]LoginAttempts{
|
||||
"admin": {
|
||||
FailCount: 1,
|
||||
},
|
||||
"test2": {
|
||||
FailCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
|
||||
for i := 0; i < 1000; i++ {
|
||||
user := pickRandomNonAdminLoginFailure(failures, "test")
|
||||
assert.Equal(t, "test2", *user)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
|
||||
failures := map[string]LoginAttempts{
|
||||
"test": {
|
||||
FailCount: 1,
|
||||
},
|
||||
"admin": {
|
||||
FailCount: 1,
|
||||
},
|
||||
"test2": {
|
||||
FailCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
|
||||
for i := 0; i < 1000; i++ {
|
||||
user := pickRandomNonAdminLoginFailure(failures, "test")
|
||||
assert.Equal(t, "test2", *user)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user