Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions internal/app/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,22 @@ var drivers = map[string]driver{
"redis": {
majorUpgradeInPlace: true,
image: "redis", port: 6379, dataPath: "/data",
health: []string{"CMD-SHELL", "redis-cli -a \"$REDIS_PASSWORD\" ping | grep -q PONG"},
// A write, not a PING. Redis answers PONG while refusing every write when
// a background save has failed and stop-writes-on-bgsave-error is on —
// which is its own default — so a connection-only probe reports healthy
// exactly when the thing callers need is gone, and a health-gated
// rollout converges onto it. SET proves the write path; EX bounds the
// key so the probe cannot accumulate one.
health: []string{"CMD-SHELL", "redis-cli -a \"$REDIS_PASSWORD\" set ob:health 1 EX 30 | grep -qx OK"},
command: []string{"sh", "-c", "exec redis-server --requirepass \"$REDIS_PASSWORD\" --appendonly yes"},
secretEnv: []string{"REDIS_PASSWORD"},
urlUser: "default", scheme: "redis", settings: settingsRedisFlag,
},
"valkey": {
majorUpgradeInPlace: true,
image: "valkey/valkey", port: 6379, dataPath: "/data",
health: []string{"CMD-SHELL", "valkey-cli -a \"$REDIS_PASSWORD\" ping | grep -q PONG"},
// Same failure mode and the same probe as redis; see the note there.
health: []string{"CMD-SHELL", "valkey-cli -a \"$REDIS_PASSWORD\" set ob:health 1 EX 30 | grep -qx OK"},
command: []string{"sh", "-c", "exec valkey-server --requirepass \"$REDIS_PASSWORD\" --appendonly yes"},
secretEnv: []string{"REDIS_PASSWORD"},
urlUser: "default", scheme: "redis", settings: settingsRedisFlag,
Expand Down
55 changes: 55 additions & 0 deletions internal/app/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,58 @@ func TestCredentialWritesAreAtomic(t *testing.T) {
t.Errorf("script still truncates a live file in place:\n%s", script)
}
}

// Redis answers PONG while refusing every write when a background save has
// failed and stop-writes-on-bgsave-error is enabled — its own default. A
// connection-only probe therefore reports the service healthy exactly when the
// behaviour callers depend on is unavailable, and a health-gated rollout
// converges onto a dependency that cannot store anything.
func TestRedisFamilyHealthChecksProveAWrite(t *testing.T) {
rendered := renderServices(t, `api_version: onebox.run/v1
app: sample
environments: {production: {server: root@h}}
workloads:
web: {role: application, image: x:1}
services:
redis: {version: 8-alpine}
valkey: {version: 8-alpine}
`)
for _, driver := range []string{"redis", "valkey"} {
doc := string(rendered[driver])
if !strings.Contains(doc, "set ob:health") {
t.Fatalf("%s health check does not write:\n%s", driver, doc)
}
// A PING-only probe is the regression this guards.
if strings.Contains(doc, "ping | grep") {
t.Fatalf("%s health check regressed to a connection-only ping:\n%s", driver, doc)
}
// Bounded, so the probe cannot accumulate keys.
if !strings.Contains(doc, "EX 30") {
t.Fatalf("%s health-check key has no TTL:\n%s", driver, doc)
}
// The generated credential still reaches the container unexpanded on the
// host: Compose reads `$$` and passes `$`.
if !strings.Contains(doc, `-a "$$REDIS_PASSWORD"`) {
t.Fatalf("%s health check lost its escaped credential reference:\n%s", driver, doc)
}
// No bare `$` of our own, which Compose would interpolate away.
probe := doc[strings.Index(doc, driver+"-cli"):]
probe = probe[:strings.Index(probe, "\n")]
if strings.Count(probe, "$")-strings.Count(probe, "$$")*2 != 0 {
t.Fatalf("%s health check carries an unescaped dollar: %s", driver, probe)
}
}
}

func renderServices(t *testing.T, src string) map[string][]byte {
t.Helper()
spec, err := LoadBytes([]byte(src), "ob.yml")
if err != nil {
t.Fatal(err)
}
r, err := spec.Render("production", "rel", nil)
if err != nil {
t.Fatal(err)
}
return r.Services
}
12 changes: 6 additions & 6 deletions internal/app/testdata/contract-verdicts.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@
{
"case": "conformance/settings key that is a real driver flag",
"loads": true,
"digest": "37ef191260abddc6f674ea8bed3bb82970358f8395df2d5a7f5b9b5142268b47 redis=bc9b8e8616909750"
"digest": "37ef191260abddc6f674ea8bed3bb82970358f8395df2d5a7f5b9b5142268b47 redis=69fece9ab514b04f"
},
{
"case": "conformance/settings key with a shell metacharacter",
Expand Down Expand Up @@ -517,12 +517,12 @@
{
"case": "corpus/authentik.yml",
"loads": true,
"digest": "6d3a2518ab077033b35018bb81aa9702a641a2ad7433468afacb6070083d60a8 postgres=dc4f8448b8b82b4a redis=2efe6d6e03c3fd6f"
"digest": "6d3a2518ab077033b35018bb81aa9702a641a2ad7433468afacb6070083d60a8 postgres=dc4f8448b8b82b4a redis=191161a0c85c6c0d"
},
{
"case": "corpus/ext-authentik-managed.yml",
"loads": true,
"digest": "f6b0b547d0bc7d55b8309e57f4f928a57e2ce44dd4fecc773599c9785580083f postgres=dc4f8448b8b82b4a redis=2efe6d6e03c3fd6f"
"digest": "f6b0b547d0bc7d55b8309e57f4f928a57e2ce44dd4fecc773599c9785580083f postgres=dc4f8448b8b82b4a redis=191161a0c85c6c0d"
},
{
"case": "corpus/ext-authentik.yml",
Expand All @@ -542,7 +542,7 @@
{
"case": "corpus/ext-immich-sourced.yml",
"loads": true,
"digest": "b852204b8ab417c0b5ca7c162108f95189829c69f8b7284d58120fe1e17c47d9 postgres=76af15324857fa90 redis=365ab102d3f39431"
"digest": "b852204b8ab417c0b5ca7c162108f95189829c69f8b7284d58120fe1e17c47d9 postgres=76af15324857fa90 redis=0281e909253b33c5"
},
{
"case": "corpus/ext-immich.yml",
Expand All @@ -552,7 +552,7 @@
{
"case": "corpus/ext-n8n.yml",
"loads": true,
"digest": "d9b22f801a91ee7c4f6e9d24811e9454374067466263cc08cbc18cb9aefc8241 postgres=809549d286e2dbdc redis=f88af25243b8688b"
"digest": "d9b22f801a91ee7c4f6e9d24811e9454374067466263cc08cbc18cb9aefc8241 postgres=809549d286e2dbdc redis=3cef59e7d2733f0a"
},
{
"case": "corpus/ext-paperless.yml",
Expand Down Expand Up @@ -612,7 +612,7 @@
{
"case": "corpus/penpot.yml",
"loads": true,
"digest": "10d0d86d56452027d9d317f6d77d0a696ed95eb2595e9cf039db6d187ff338e6 postgres=fc584b1b50db23a6 redis=5e6b9f3ca2de9a2f"
"digest": "10d0d86d56452027d9d317f6d77d0a696ed95eb2595e9cf039db6d187ff338e6 postgres=fc584b1b50db23a6 redis=be4534525f623f76"
},
{
"case": "corpus/pursue.yml",
Expand Down