mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
feat(redis): Secrets credentials via volume mount (#24597)
Signed-off-by: Mangaal <angommeeteimangaal@gmail.com> Co-authored-by: Nitish Kumar <justnitish06@gmail.com>
This commit is contained in:
59
docs/faq.md
59
docs/faq.md
@@ -328,10 +328,69 @@ If for some reason authenticated Redis does not work for you and you want to use
|
||||
* Deployment: argocd-server
|
||||
* StatefulSet: argocd-application-controller
|
||||
|
||||
5. If you have configured file-based Redis credentials using the `REDIS_CREDS_DIR_PATH` environment variable, remove this environment variable and delete the corresponding volume and volumeMount entries that mount the credentials directory from the following manifests:
|
||||
* Deployment: argocd-repo-server
|
||||
* Deployment: argocd-server
|
||||
* StatefulSet: argocd-application-controller
|
||||
|
||||
## How do I provide my own Redis credentials?
|
||||
The Redis password is stored in Kubernetes secret `argocd-redis` with key `auth` in the namespace where Argo CD is installed.
|
||||
You can config your secret provider to generate Kubernetes secret accordingly.
|
||||
|
||||
### Using file-based Redis credentials via `REDIS_CREDS_DIR_PATH`
|
||||
|
||||
Argo CD components support reading Redis credentials from files mounted at a specified path inside the container.
|
||||
|
||||
When the environment variable `REDIS_CREDS_DIR_PATH` is specified, it takes precedence and Argo CD components that require redis connectivity ( application-controller, repo-server and server) loads the redis credentials from the files located in the specified directory path and ignores any values set in the environment variables
|
||||
|
||||
Expected files when using `REDIS_CREDS_DIR_PATH`:
|
||||
|
||||
- `auth`: Redis password (mandatory)
|
||||
- `auth_username`: Redis username
|
||||
- `sentinel_auth`: Redis Sentinel password
|
||||
- `sentinel_username`: Redis Sentinel username
|
||||
|
||||
You can store these keys in a Kubernetes Secret and mount it into each Argo CD component that needs Redis access. Then point `REDIS_CREDS_DIR_PATH` to the mount directory.
|
||||
|
||||
Example Secret:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
namespace: argocd
|
||||
type: Opaque
|
||||
stringData:
|
||||
auth: "<redis-password>"
|
||||
auth_username: "<redis-username>"
|
||||
sentinel_auth: "<sentinel-password>"
|
||||
sentinel_username: "<sentinel-username>"
|
||||
```
|
||||
|
||||
Example Argo CD component spec (e.g., add to `argocd-server`, `argocd-repo-server`, `argocd-application-controller`):
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-server
|
||||
image: quay.io/argoproj/argocd:<version>
|
||||
env:
|
||||
- name: REDIS_CREDS_DIR_PATH
|
||||
value: "/var/run/secrets/redis"
|
||||
volumeMounts:
|
||||
- name: redis-creds
|
||||
mountPath: "/var/run/secrets/redis"
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: redis-creds
|
||||
secret:
|
||||
secretName: <secret-name>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This mechanism configures authentication for Argo CD components that connect to Redis. The Redis server itself should be configured independently (e.g., via `redis.conf`).
|
||||
|
||||
## How do I fix `Manifest generation error (cached)`?
|
||||
|
||||
`Manifest generation error (cached)` means that there was an error when generating manifests and that the error message has been cached to avoid runaway retries.
|
||||
|
||||
102
util/cache/cache.go
vendored
102
util/cache/cache.go
vendored
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +34,8 @@ const (
|
||||
envRedisSentinelPassword = "REDIS_SENTINEL_PASSWORD"
|
||||
// envRedisSentinelUsername is an env variable name which stores redis sentinel username
|
||||
envRedisSentinelUsername = "REDIS_SENTINEL_USERNAME"
|
||||
// envRedisCredsFilePath is an env variable name which stores path to redis credentials file
|
||||
envRedisCredsDirPath = "REDIS_CREDS_DIR_PATH"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -129,6 +132,81 @@ func getFlagVal[T any](cmd *cobra.Command, o Options, name string, getVal func(n
|
||||
}
|
||||
}
|
||||
|
||||
// loadRedisCreds loads Redis credentials either from file-based mounts or environment variables.
|
||||
// If a mount path is provided, Redis credentials are expected to be read only from the mounted files.
|
||||
// If no mount path is provided, the function falls back to reading credentials from environment variables
|
||||
// to maintain backward compatibility.
|
||||
func loadRedisCreds(mountPath string, opt Options) (username, password, sentinelUsername, sentinelPassword string, err error) {
|
||||
if mountPath != "" {
|
||||
log.Infof("Loading Redis credentials from mounted directory: %s", mountPath)
|
||||
if _, statErr := os.Stat(mountPath); statErr != nil {
|
||||
return "", "", "", "", fmt.Errorf("failed to access Redis credentials: mount path %q does not exist or is inaccessible: %w", mountPath, statErr)
|
||||
}
|
||||
password, err = readAuthDetailsFromFile(mountPath, "auth")
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
username, err = readAuthDetailsFromFile(mountPath, "auth_username")
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
sentinelUsername, err = readAuthDetailsFromFile(mountPath, "sentinel_username")
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
sentinelPassword, err = readAuthDetailsFromFile(mountPath, "sentinel_auth")
|
||||
if err != nil {
|
||||
return "", "", "", "", err
|
||||
}
|
||||
|
||||
return username, password, sentinelUsername, sentinelPassword, nil
|
||||
}
|
||||
log.Info("Loading Redis credentials from environment variables")
|
||||
username = os.Getenv(envRedisUsername)
|
||||
password = os.Getenv(envRedisPassword)
|
||||
sentinelUsername = os.Getenv(envRedisSentinelUsername)
|
||||
sentinelPassword = os.Getenv(envRedisSentinelPassword)
|
||||
// If a flag prefix is set, prefer prefixed env vars to allow component-specific overrides (e.g., REPOSERVER_REDIS_PASSWORD).
|
||||
if opt.FlagPrefix != "" {
|
||||
pref := opt.getEnvPrefix()
|
||||
if val := os.Getenv(pref + envRedisUsername); val != "" {
|
||||
username = val
|
||||
}
|
||||
if val := os.Getenv(pref + envRedisPassword); val != "" {
|
||||
password = val
|
||||
}
|
||||
if val := os.Getenv(pref + envRedisSentinelUsername); val != "" {
|
||||
sentinelUsername = val
|
||||
}
|
||||
if val := os.Getenv(pref + envRedisSentinelPassword); val != "" {
|
||||
sentinelPassword = val
|
||||
}
|
||||
}
|
||||
return username, password, sentinelUsername, sentinelPassword, nil
|
||||
}
|
||||
|
||||
// readAuthDetailsFromFile reads authentication file from the given
|
||||
// mount path. If the file does not exist, it returns an empty string and no error.
|
||||
// which is the expected behavior for optional secrets.
|
||||
//
|
||||
// An error is returned only when the file exists but cannot be accessed (e.g.,
|
||||
// permission issues or other filesystem errors). This helps distinguish between
|
||||
// a missing optional credential (valid case) and a real misconfiguration
|
||||
func readAuthDetailsFromFile(mountPath, filename string) (string, error) {
|
||||
path := filepath.Join(mountPath, filename)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Expected when a particular credential is not used
|
||||
log.Infof("Redis credential file %s not found; using empty value for Redis credential %s", path, filename)
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("failed to access Redis credential file %s: %w", path, err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(data)), nil
|
||||
}
|
||||
|
||||
// AddCacheFlagsToCmd adds flags which control caching to the specified command
|
||||
func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...Options) func() (*Cache, error) {
|
||||
redisAddress := ""
|
||||
@@ -206,25 +284,17 @@ func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...Options) func() (*Cache, err
|
||||
}
|
||||
}
|
||||
}
|
||||
password := os.Getenv(envRedisPassword)
|
||||
username := os.Getenv(envRedisUsername)
|
||||
sentinelUsername := os.Getenv(envRedisSentinelUsername)
|
||||
sentinelPassword := os.Getenv(envRedisSentinelPassword)
|
||||
var password, username, sentinelUsername, sentinelPassword string
|
||||
credsDirPath := os.Getenv(envRedisCredsDirPath)
|
||||
if opt.FlagPrefix != "" {
|
||||
if val := os.Getenv(opt.getEnvPrefix() + envRedisUsername); val != "" {
|
||||
username = val
|
||||
}
|
||||
if val := os.Getenv(opt.getEnvPrefix() + envRedisPassword); val != "" {
|
||||
password = val
|
||||
}
|
||||
if val := os.Getenv(opt.getEnvPrefix() + envRedisSentinelUsername); val != "" {
|
||||
sentinelUsername = val
|
||||
}
|
||||
if val := os.Getenv(opt.getEnvPrefix() + envRedisSentinelPassword); val != "" {
|
||||
sentinelPassword = val
|
||||
if val := os.Getenv(opt.getEnvPrefix() + envRedisCredsDirPath); val != "" {
|
||||
credsDirPath = val
|
||||
}
|
||||
}
|
||||
|
||||
username, password, sentinelUsername, sentinelPassword, err := loadRedisCreds(credsDirPath, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxRetries := env.ParseNumFromEnv(envRedisRetryCount, defaultRedisRetryCount, 0, math.MaxInt32)
|
||||
compression, err := CompressionTypeFromString(compressionStr)
|
||||
if err != nil {
|
||||
|
||||
98
util/cache/cache_test.go
vendored
98
util/cache/cache_test.go
vendored
@@ -1,6 +1,8 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -87,3 +89,99 @@ func TestGenerateCacheKey(t *testing.T) {
|
||||
testKey := cache.generateFullKey("testkey")
|
||||
assert.Equal(t, "testkey|"+common.CacheVersion, testKey)
|
||||
}
|
||||
|
||||
// Test loading Redis credentials from a file
|
||||
func TestLoadRedisCreds(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Helper to write a file
|
||||
writeFile := func(name, content string) {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o400))
|
||||
}
|
||||
// Write all files
|
||||
writeFile("auth", "mypassword\n")
|
||||
writeFile("auth_username", "myuser")
|
||||
writeFile("sentinel_username", "sentineluser")
|
||||
writeFile("sentinel_auth", "sentinelpass")
|
||||
|
||||
username, password, sentinelUsername, sentinelPassword, err := loadRedisCreds(dir, Options{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "mypassword", password)
|
||||
assert.Equal(t, "myuser", username)
|
||||
assert.Equal(t, "sentineluser", sentinelUsername)
|
||||
assert.Equal(t, "sentinelpass", sentinelPassword)
|
||||
}
|
||||
|
||||
// Test loading Redis credentials from environment variables
|
||||
func TestLoadRedisCredsFromEnv(t *testing.T) {
|
||||
// Set environment variables
|
||||
t.Setenv(envRedisPassword, "mypassword")
|
||||
t.Setenv(envRedisUsername, "myuser")
|
||||
t.Setenv(envRedisSentinelUsername, "sentineluser")
|
||||
t.Setenv(envRedisSentinelPassword, "sentinelpass")
|
||||
|
||||
username, password, sentinelUsername, sentinelPassword, err := loadRedisCreds("", Options{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "mypassword", password)
|
||||
assert.Equal(t, "myuser", username)
|
||||
assert.Equal(t, "sentineluser", sentinelUsername)
|
||||
assert.Equal(t, "sentinelpass", sentinelPassword)
|
||||
}
|
||||
|
||||
// Test loading Redis credentials from both environment variables and a file
|
||||
func TestLoadRedisCredsFromBothEnvAndFile(t *testing.T) {
|
||||
// Set environment variables
|
||||
t.Setenv(envRedisPassword, "mypassword")
|
||||
t.Setenv(envRedisUsername, "myuser")
|
||||
t.Setenv(envRedisSentinelUsername, "sentineluser")
|
||||
t.Setenv(envRedisSentinelPassword, "sentinelpass")
|
||||
|
||||
dir := t.TempDir()
|
||||
// Helper to write a file
|
||||
writeFile := func(name, content string) {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o400))
|
||||
}
|
||||
// Write all files
|
||||
writeFile("auth", "filepassword")
|
||||
writeFile("auth_username", "fileuser")
|
||||
writeFile("sentinel_username", "filesentineluser")
|
||||
writeFile("sentinel_auth", "filesentinelpass")
|
||||
|
||||
username, password, sentinelUsername, sentinelPassword, err := loadRedisCreds(dir, Options{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "filepassword", password)
|
||||
assert.Equal(t, "fileuser", username)
|
||||
assert.Equal(t, "filesentineluser", sentinelUsername)
|
||||
assert.Equal(t, "filesentinelpass", sentinelPassword)
|
||||
}
|
||||
|
||||
func TestLoadRedisCreds_MountPathMissing(t *testing.T) {
|
||||
_, _, _, _, err := loadRedisCreds("not_existing_path", Options{})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to access Redis credentials")
|
||||
}
|
||||
|
||||
func TestCredentialFileHandling(t *testing.T) {
|
||||
t.Run("ReadAuthDetailsFromFile_Missing", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
val, err := readAuthDetailsFromFile(dir, "not_existing_path")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, val)
|
||||
})
|
||||
|
||||
t.Run("ReadAuthDetailsFromFile_Unreadable", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "auth")
|
||||
require.NoError(t, os.WriteFile(file, []byte("value"), 0o000))
|
||||
_, err := readAuthDetailsFromFile(dir, "auth")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("ReadAuthDetailsFromFile_Normal", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "auth")
|
||||
require.NoError(t, os.WriteFile(file, []byte("value"), 0o400))
|
||||
val, err := readAuthDetailsFromFile(dir, "auth")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "value", val)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user