a466b3e07f
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
134 lines
2.9 KiB
Go
134 lines
2.9 KiB
Go
package driver
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.unkin.net/unkin/waitfordb/internal/config"
|
|
)
|
|
|
|
func init() { register(valkey{}) }
|
|
|
|
type valkey struct{}
|
|
|
|
func (valkey) Name() string { return "valkey" }
|
|
|
|
func (valkey) Open(cfg config.Config) (Pinger, error) {
|
|
p := &valkeyPinger{
|
|
addr: net.JoinHostPort(cfg.Host, cfg.Port),
|
|
user: cfg.User,
|
|
password: cfg.Password,
|
|
}
|
|
if cfg.DSN != "" {
|
|
u, err := url.Parse(cfg.DSN)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid valkey DSN: %w", err)
|
|
}
|
|
if u.Scheme != "redis" && u.Scheme != "valkey" {
|
|
return nil, fmt.Errorf("unsupported valkey DSN scheme %q (use redis:// or valkey://)", u.Scheme)
|
|
}
|
|
port := u.Port()
|
|
if port == "" {
|
|
port = "6379"
|
|
}
|
|
p.addr = net.JoinHostPort(u.Hostname(), port)
|
|
if u.User != nil {
|
|
p.user = u.User.Username()
|
|
if pw, ok := u.User.Password(); ok {
|
|
p.password = pw
|
|
}
|
|
}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// valkeyPinger speaks just enough RESP to AUTH and PING. Each Ping uses a
|
|
// fresh connection so a half-open socket from a failed attempt cannot linger.
|
|
type valkeyPinger struct {
|
|
addr string
|
|
user string
|
|
password string
|
|
}
|
|
|
|
func (p *valkeyPinger) Ping(ctx context.Context) error {
|
|
var d net.Dialer
|
|
conn, err := d.DialContext(ctx, "tcp", p.addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
if dl, ok := ctx.Deadline(); ok {
|
|
_ = conn.SetDeadline(dl)
|
|
}
|
|
|
|
r := bufio.NewReader(conn)
|
|
if p.password != "" {
|
|
args := []string{"AUTH"}
|
|
if p.user != "" && p.user != "default" {
|
|
args = append(args, p.user)
|
|
}
|
|
args = append(args, p.password)
|
|
if _, err := roundTrip(conn, r, args...); err != nil {
|
|
return fmt.Errorf("AUTH %s: %w", p.user, err)
|
|
}
|
|
}
|
|
|
|
reply, err := roundTrip(conn, r, "PING")
|
|
if err != nil {
|
|
return fmt.Errorf("PING: %w", err)
|
|
}
|
|
if reply != "PONG" {
|
|
return fmt.Errorf("PING: unexpected reply %q", reply)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *valkeyPinger) Close() error { return nil }
|
|
|
|
func roundTrip(w io.Writer, r *bufio.Reader, args ...string) (string, error) {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "*%d\r\n", len(args))
|
|
for _, a := range args {
|
|
fmt.Fprintf(&b, "$%d\r\n%s\r\n", len(a), a)
|
|
}
|
|
if _, err := io.WriteString(w, b.String()); err != nil {
|
|
return "", err
|
|
}
|
|
return readReply(r)
|
|
}
|
|
|
|
func readReply(r *bufio.Reader) (string, error) {
|
|
line, err := r.ReadString('\n')
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
line = strings.TrimRight(line, "\r\n")
|
|
if line == "" {
|
|
return "", fmt.Errorf("empty reply")
|
|
}
|
|
switch line[0] {
|
|
case '+':
|
|
return line[1:], nil
|
|
case '-':
|
|
return "", fmt.Errorf("server error: %s", line[1:])
|
|
case '$':
|
|
n, err := strconv.Atoi(line[1:])
|
|
if err != nil || n < 0 {
|
|
return "", fmt.Errorf("unexpected reply %q", line)
|
|
}
|
|
buf := make([]byte, n+2) // payload + trailing CRLF
|
|
if _, err := io.ReadFull(r, buf); err != nil {
|
|
return "", err
|
|
}
|
|
return string(buf[:n]), nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected reply %q", line)
|
|
}
|
|
}
|