Add valkey/redis driver with operator-secret auto-configuration
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/pre-commit Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

Apps fronted by valkey-operator instances need the same wait-for-ready
initContainer postgres workloads already get, and wiring per-field
secretKeyRefs for operator-generated secrets is boilerplate. waitfordb
now speaks enough RESP to AUTH and PING, and natively understands the
secret shapes CNPG and valkey-operator generate so an initContainer is
just envFrom plus a mode variable.

- valkey driver (alias: redis): fresh TCP connection per attempt,
  optional AUTH (ACL user or default), PING, reusing the existing
  retry/backoff wait loop; redis:///valkey:// DSNs; default port 6379
- WAITFORDB_SECRET_FORMAT=cnpg|valkey for envFrom-injected operator
  secrets: CNPG <cluster>-app host/port/dbname/user/password keys, and
  valkey-operator key-per-username secrets (_operator preferred, or
  WAITFORDB_USER's same-named key)
- autodetection from injected keys (CNPG keys -> postgres, valkey keys
  -> valkey); explicit WAITFORDB_DRIVER/WAITFORDB_* always win, PG*
  fallback and all existing flags unchanged
- valkey needs no user/database to be valid (unauthenticated PING)
- tests: secret shape parsing/autodetect/mismatch, fake RESP server
  covering NOAUTH/WRONGPASS/ACL auth/DSN, retry-until-up wait
- README: envFrom initContainer snippets for CNPG and valkey-operator
This commit is contained in:
unkin-agent
2026-08-23 16:36:52 +10:00
parent 8654dab582
commit a466b3e07f
7 changed files with 720 additions and 46 deletions
+123 -28
View File
@@ -20,12 +20,24 @@ const (
var defaultPort = map[string]string{
"postgres": "5432",
"mysql": "3306",
"valkey": "6379",
}
// Secret formats natively understood via WAITFORDB_SECRET_FORMAT (or
// autodetection): operator-generated secrets injected wholesale with envFrom.
const (
FormatCNPG = "cnpg" // CNPG <cluster>-app secret: host/port/dbname/user/password keys
FormatValkey = "valkey" // valkey-operator user secrets: key = username, value = password
)
// Config is the resolved, validated configuration for a single run.
type Config struct {
Driver string
// SecretFormat records which operator secret convention supplied the
// connection parameters ("" when none).
SecretFormat string
// DSN, when set, is a full driver-native connection string that overrides
// the discrete Host/Port/User/Password/Database fields.
DSN string
@@ -57,26 +69,79 @@ func errf(format string, a ...any) *Error { return &Error{msg: fmt.Sprintf(forma
type Getenv func(string) string
// Load resolves configuration from env. Connection-parameter precedence is
// DSN > WAITFORDB_* > PG* (the libpq fallback applies to the postgres driver
// only).
// DSN > WAITFORDB_* > PG* > operator-secret keys (the libpq fallback applies
// to the postgres driver only).
func Load(get Getenv) (Config, error) {
c := Config{
Driver: firstNonEmpty(get("WAITFORDB_DRIVER"), DefaultDriver),
Driver: strings.ToLower(strings.TrimSpace(get("WAITFORDB_DRIVER"))),
DSN: get("WAITFORDB_DSN"),
ConnectTimeout: DefaultConnectTimeout,
Interval: DefaultInterval,
}
c.Driver = strings.ToLower(strings.TrimSpace(c.Driver))
if c.Driver == "redis" {
c.Driver = "valkey"
}
pg := c.Driver == "postgres"
c.Host = get("WAITFORDB_HOST")
c.Port = get("WAITFORDB_PORT")
c.User = get("WAITFORDB_USER")
c.Password = get("WAITFORDB_PASSWORD")
c.Database = get("WAITFORDB_DATABASE")
c.SSLMode = get("WAITFORDB_SSLMODE")
// WAITFORDB_* first, then the libpq PG* fallback for postgres.
c.Host = pick(get, pg, "WAITFORDB_HOST", "PGHOST")
c.Port = pick(get, pg, "WAITFORDB_PORT", "PGPORT")
c.User = pick(get, pg, "WAITFORDB_USER", "PGUSER")
c.Password = pick(get, pg, "WAITFORDB_PASSWORD", "PGPASSWORD")
c.Database = pick(get, pg, "WAITFORDB_DATABASE", "PGDATABASE")
c.SSLMode = pick(get, pg, "WAITFORDB_SSLMODE", "PGSSLMODE")
format := strings.ToLower(strings.TrimSpace(get("WAITFORDB_SECRET_FORMAT")))
formatExplicit := format != ""
if formatExplicit {
if format != FormatCNPG && format != FormatValkey {
return Config{}, errf("unsupported WAITFORDB_SECRET_FORMAT %q (supported: %s, %s)", format, FormatCNPG, FormatValkey)
}
} else {
format = detectFormat(get, c)
}
// An explicit driver wins; otherwise the secret format implies it, falling
// back to the historical postgres default.
if c.Driver == "" {
switch format {
case FormatValkey:
c.Driver = "valkey"
default:
c.Driver = DefaultDriver
}
}
if formatExplicit && !formatMatchesDriver(format, c.Driver) {
return Config{}, errf("WAITFORDB_SECRET_FORMAT %q does not apply to driver %q", format, c.Driver)
}
if formatMatchesDriver(format, c.Driver) {
c.SecretFormat = format
}
// The libpq PG* fallback for still-unset postgres fields.
if c.Driver == "postgres" {
fill(&c.Host, get("PGHOST"))
fill(&c.Port, get("PGPORT"))
fill(&c.User, get("PGUSER"))
fill(&c.Password, get("PGPASSWORD"))
fill(&c.Database, get("PGDATABASE"))
fill(&c.SSLMode, get("PGSSLMODE"))
}
// Operator-secret keys fill whatever is still unset.
switch c.SecretFormat {
case FormatCNPG:
fill(&c.Host, get("host"))
fill(&c.Port, get("port"))
fill(&c.Database, get("dbname"))
fill(&c.User, firstNonEmpty(get("user"), get("username")))
fill(&c.Password, get("password"))
case FormatValkey:
if c.User == "" && get("_operator") != "" {
c.User = "_operator"
}
if c.Password == "" && c.User != "" {
c.Password = get(c.User)
}
}
if c.Host == "" {
c.Host = DefaultHost
@@ -102,9 +167,44 @@ func Load(get Getenv) (Config, error) {
return c, nil
}
// detectFormat recognises operator-generated secrets injected via envFrom.
// CNPG <cluster>-app secrets carry lowercase host/dbname/user keys; valkey
// user secrets carry one key per username (system users start with "_").
func detectFormat(get Getenv, c Config) string {
if get("host") != "" && get("dbname") != "" && (get("user") != "" || get("username") != "") {
return FormatCNPG
}
if get("_operator") != "" || get("_replication") != "" {
return FormatValkey
}
// An explicit ACL user whose password key is present, with no SQL database
// configured anywhere, is the valkey users-secret shape.
if (c.Driver == "" || c.Driver == "valkey") && c.User != "" && c.Password == "" &&
c.Database == "" && get("PGDATABASE") == "" && get(c.User) != "" {
return FormatValkey
}
return ""
}
func formatMatchesDriver(format, driver string) bool {
switch format {
case FormatCNPG:
return driver == "postgres"
case FormatValkey:
return driver == "valkey"
}
return false
}
func fill(dst *string, v string) {
if *dst == "" {
*dst = v
}
}
func (c Config) validate() error {
if _, ok := defaultPort[c.Driver]; !ok {
return errf("unsupported WAITFORDB_DRIVER %q (supported: postgres, mysql)", c.Driver)
return errf("unsupported WAITFORDB_DRIVER %q (supported: postgres, mysql, valkey/redis)", c.Driver)
}
if c.Interval <= 0 {
return errf("WAITFORDB_INTERVAL must be > 0")
@@ -116,7 +216,8 @@ func (c Config) validate() error {
return errf("WAITFORDB_TIMEOUT must be >= 0")
}
// With a DSN the discrete fields are optional (the DSN carries them).
if c.DSN == "" {
// Valkey needs neither a database nor a user (unauthenticated PING is valid).
if c.DSN == "" && c.Driver != "valkey" {
if c.Database == "" {
return errf("no database configured: set WAITFORDB_DATABASE (or PGDATABASE for postgres) or WAITFORDB_DSN")
}
@@ -132,11 +233,17 @@ func (c Config) validate() error {
func (c Config) Redacted() string {
var b strings.Builder
fmt.Fprintf(&b, "driver=%s", c.Driver)
if c.SecretFormat != "" {
fmt.Fprintf(&b, " secret_format=%s", c.SecretFormat)
}
if c.DSN != "" {
fmt.Fprintf(&b, " dsn=%s", redactDSN(c.DSN))
} else {
fmt.Fprintf(&b, " addr=%s:%s database=%s user=%s password=%s",
c.Host, c.Port, c.Database, c.User, redactSecret(c.Password))
fmt.Fprintf(&b, " addr=%s:%s", c.Host, c.Port)
if c.Database != "" {
fmt.Fprintf(&b, " database=%s", c.Database)
}
fmt.Fprintf(&b, " user=%s password=%s", c.User, redactSecret(c.Password))
if c.SSLMode != "" {
fmt.Fprintf(&b, " sslmode=%s", c.SSLMode)
}
@@ -202,18 +309,6 @@ func redactKeyword(dsn, key string) string {
return dsn[:valStart] + "***" + dsn[valEnd:]
}
// pick returns the WAITFORDB_* value, falling back to the PG* value only when
// fallback is true (postgres).
func pick(get Getenv, fallback bool, primary, secondary string) string {
if v := get(primary); v != "" {
return v
}
if fallback {
return get(secondary)
}
return ""
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
+156
View File
@@ -171,3 +171,159 @@ func TestRedactedDSNMasksPassword(t *testing.T) {
})
}
}
func TestCNPGAutodetect(t *testing.T) {
// Shape of a CNPG <cluster>-app secret injected wholesale via envFrom.
c, err := Load(envFrom(map[string]string{
"host": "mydb-rw.ns.svc",
"port": "5432",
"dbname": "appdb",
"user": "appuser",
"username": "appuser",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "postgres" || c.SecretFormat != FormatCNPG {
t.Errorf("driver=%q format=%q, want postgres/cnpg", c.Driver, c.SecretFormat)
}
if c.Host != "mydb-rw.ns.svc" || c.Database != "appdb" || c.User != "appuser" || c.Password != "pw" {
t.Errorf("fields not filled from CNPG keys: %+v", c)
}
}
func TestCNPGExplicitFormat(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_SECRET_FORMAT": "cnpg",
"host": "h",
"dbname": "d",
"username": "u",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.SecretFormat != FormatCNPG || c.User != "u" {
t.Errorf("format=%q user=%q, want cnpg/u", c.SecretFormat, c.User)
}
}
func TestWaitfordbOverridesCNPGKeys(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_HOST": "explicit-host",
"host": "secret-host",
"dbname": "d",
"user": "u",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Host != "explicit-host" {
t.Errorf("host = %q, want explicit-host (WAITFORDB_* wins over secret keys)", c.Host)
}
if c.Database != "d" || c.Password != "pw" {
t.Errorf("unset fields should still fill from secret: %+v", c)
}
}
func TestValkeySystemSecretAutodetect(t *testing.T) {
// Shape of the valkey-operator system-passwords secret: key = username.
c, err := Load(envFrom(map[string]string{
"WAITFORDB_HOST": "myapp-valkey.ns.svc",
"_operator": "op-pass",
"_replication": "repl-pass",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "valkey" || c.SecretFormat != FormatValkey {
t.Errorf("driver=%q format=%q, want valkey/valkey", c.Driver, c.SecretFormat)
}
if c.User != "_operator" || c.Password != "op-pass" {
t.Errorf("user=%q password=%q, want _operator/op-pass", c.User, c.Password)
}
if c.Port != "6379" {
t.Errorf("port = %q, want default 6379", c.Port)
}
}
func TestValkeyACLUserSecret(t *testing.T) {
// An explicit ACL username whose password arrives as an env key of the
// same name (valkey-operator user secret via envFrom).
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "valkey",
"WAITFORDB_USER": "appuser",
"appuser": "app-pass",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.SecretFormat != FormatValkey || c.Password != "app-pass" {
t.Errorf("format=%q password=%q, want valkey/app-pass", c.SecretFormat, c.Password)
}
}
func TestValkeyExplicitFlagsNoSecret(t *testing.T) {
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "valkey",
"WAITFORDB_HOST": "h",
"WAITFORDB_PASSWORD": "pw",
}))
if err != nil {
t.Fatalf("valkey needs no user/database: %v", err)
}
if c.SecretFormat != "" {
t.Errorf("format = %q, want none without operator secret keys", c.SecretFormat)
}
}
func TestRedisDriverAlias(t *testing.T) {
c, err := Load(envFrom(map[string]string{"WAITFORDB_DRIVER": "redis"}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Driver != "valkey" || c.Port != "6379" {
t.Errorf("driver=%q port=%q, want valkey/6379", c.Driver, c.Port)
}
}
func TestExplicitDriverIgnoresMismatchedDetect(t *testing.T) {
// CNPG-shaped keys with an explicit valkey driver: format must not apply.
c, err := Load(envFrom(map[string]string{
"WAITFORDB_DRIVER": "valkey",
"host": "secret-host",
"dbname": "d",
"user": "u",
"password": "pw",
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.SecretFormat != "" {
t.Errorf("format = %q, want none (cnpg does not apply to valkey)", c.SecretFormat)
}
if c.Host != DefaultHost {
t.Errorf("host = %q, want default (secret keys must not fill)", c.Host)
}
}
func TestSecretFormatErrors(t *testing.T) {
cases := map[string]map[string]string{
"unknown format": {"WAITFORDB_SECRET_FORMAT": "vault"},
"format/driver mismatch": {
"WAITFORDB_SECRET_FORMAT": "cnpg",
"WAITFORDB_DRIVER": "valkey",
},
}
for name, env := range cases {
t.Run(name, func(t *testing.T) {
if _, err := Load(envFrom(env)); err == nil {
t.Fatalf("expected error for %s", name)
} else if _, ok := err.(*Error); !ok {
t.Fatalf("expected *config.Error, got %T", err)
}
})
}
}