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:
Mangaal Meetei
2025-11-24 18:18:15 +05:30
committed by GitHub
parent 14d05d2cea
commit fe02a8f410
3 changed files with 243 additions and 16 deletions

View File

@@ -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
View File

@@ -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 {

View File

@@ -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)
})
}