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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ The test suite uses mock backends for almost all tests, so `go test ./...` passe
| `CUSTOMER_REDIS_URL` | Admin URL for redis-provision | `redis://redis-provision.instant-data.svc.cluster.local:6379` |
| `CUSTOMER_MONGO_URL` | Admin URL for mongodb | `mongodb://root:root@mongodb.instant-data.svc.cluster.local:27017` |
| `POSTGRES_CLUSTER_URLS` | Comma-separated list of admin DSNs (multi-cluster) | unset |
| `REDIS_PROVISION_URL` | **Credentialed** admin URL for the shared redis-provision pool: `redis://[user]:password@host:port[/db]`. Required when the pool runs with `--requirepass` — without it `ACL SETUSER` fails and `/cache/new` 503s. Supersedes `REDIS_PROVISION_HOST`; a malformed value logs an error and falls back to it | unset |
| `REDIS_PROVISION_HOST` | Bare `host:port` admin address for the shared Redis pool. Sends no AUTH — legacy / unauthenticated pools only | `localhost:6379` |
| `MONGO_PUBLIC_HOST_PORT`, `MONGO_PUBLIC_HOST` (+ `MONGO_PUBLIC_PORT`) | Customer-facing host embedded in `/nosql/new` URLs on the shared backend. Falls back to `K8S_MONGO_PUBLIC_HOST`, then to the in-cluster admin host | unset (port `27017`) |
| `NATS_PUBLIC_HOST_PORT`, `NATS_PUBLIC_HOST` (+ `NATS_PUBLIC_PORT`) | Customer-facing host embedded in `/queue/new` URLs on the shared backend. Falls back to `K8S_NATS_PUBLIC_HOST`, then to the in-cluster admin host | unset (port `4222`) |
| `K8S_DEDICATED_BACKEND` | Enable k8s dedicated-pod backend for team / growth tier | `false` |
| `K8S_EXTERNAL_HOST` | External hostname for dedicated k8s services | unset |
| `K8S_STORAGE_CLASS` | Storage class for dedicated PVCs | `local-path` |
Expand Down
65 changes: 64 additions & 1 deletion internal/backend/mongo/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"encoding/hex"
"fmt"
"log/slog"
"os"
"time"

"go.mongodb.org/mongo-driver/bson"
Expand All @@ -28,6 +29,68 @@ import (
// Short to fail-fast in tests and when MongoDB is not reachable.
const connectTimeout = 3 * time.Second

// defaultMongoPort is appended to a public hostname that carries no port of its
// own. 27017 is the MongoDB wire default and what the mongo-proxy listens on —
// the same port the k8s backend hardcodes when building customer URLs (k8s.go).
const defaultMongoPort = "27017"

// buildMongoURL constructs the user-facing connection URL for a provisioned
// database. Mirrors postgres.buildDBURL (backend/postgres/local.go): the public
// host wins when configured, otherwise clusterHost — the in-cluster admin
// address, which is only resolvable from inside the cluster.
//
// This is the fix for the leak of internal cluster DNS into customer
// connection strings: before it, /nosql/new handed out
// mongodb://…@mongodb.instant-data.svc.cluster.local:27017/… on the shared
// backend, because the public host was applied only in the "k8s" branch of
// NewBackend and the cluster runs MONGO_PROVISION_BACKEND=local.
func buildMongoURL(clusterHost, username, password, dbName string) string {
host := publicHostPort()
if host == "" {
host = clusterHost
}
return fmt.Sprintf("mongodb://%s:%s@%s/%s?authSource=admin", username, password, host, dbName)
}

// publicHostPort returns the host:port to embed in user-facing MongoDB URLs, or
// "" when no public host is configured (the caller then falls back to the
// cluster-internal mongoHost).
//
// Identical mechanism to postgres.publicHostPort (backend/postgres/local.go) and
// redis.publicHostPort (backend/redis/local.go) — env-resolved at Provision
// time, never a constructor argument, so the shared/local backend and the
// dedicated k8s backend agree on the customer-facing hostname.
//
// Resolution order:
// 1. MONGO_PUBLIC_HOST_PORT (e.g. "mongo.instanode.dev:27017")
// 2. MONGO_PUBLIC_HOST + MONGO_PUBLIC_PORT (port defaults to 27017)
// 3. K8S_MONGO_PUBLIC_HOST + MONGO_PUBLIC_PORT — the env the k8s branch of
// NewBackend already reads. Honouring it here is what makes the fix a pure
// code change: a cluster that already advertises mongo.instanode.dev for
// dedicated pods now advertises it for shared ones too, no ops change.
// 4. "" — caller falls back to the in-cluster mongoHost.
//
// Deliberately NO built-in default (the k8s branch defaults to
// "mongo.instanode.dev"): a dev box running the shared backend against
// localhost:27017 must keep emitting localhost, not a production hostname.
func publicHostPort() string {
if hp := os.Getenv("MONGO_PUBLIC_HOST_PORT"); hp != "" {
return hp
}
host := os.Getenv("MONGO_PUBLIC_HOST")
if host == "" {
host = os.Getenv("K8S_MONGO_PUBLIC_HOST")
}
if host == "" {
return ""
}
port := os.Getenv("MONGO_PUBLIC_PORT")
if port == "" {
port = defaultMongoPort
}
return host + ":" + port
}

// decodeStorageSize extracts the dbStats storageSize from a decoded result,
// tolerating every BSON numeric encoding the server may use across versions
// (int32 / int64 / float64). Any missing or non-numeric value yields 0 — the
Expand Down Expand Up @@ -140,7 +203,7 @@ func (b *LocalBackend) Provision(ctx context.Context, token, tier string) (*Cred
}

// User is created in the admin database; include authSource so clients authenticate correctly.
url := fmt.Sprintf("mongodb://%s:%s@%s/%s?authSource=admin", username, password, b.mongoHost, dbName)
url := buildMongoURL(b.mongoHost, username, password, dbName)
slog.Info("nosql.Provision: provisioned",
"token", token,
"db", dbName,
Expand Down
183 changes: 183 additions & 0 deletions internal/backend/mongo/public_host_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package mongo

// public_host_test.go — the customer-facing hostname in /nosql/new connection
// strings.
//
// COVERAGE BLOCK (CLAUDE.md rule 17):
//
// Symptom: /nosql/new returned
// mongodb://usr_…:…@mongodb.instant-data.svc.cluster.local:27017/…
// — internal cluster DNS no customer can resolve. The public
// host was applied only inside the `case "k8s"` branch of
// NewBackend (backend.go), and the cluster runs
// MONGO_PROVISION_BACKEND=local.
// Enumeration: rg -F 'mongodb://' / 'b.mongoHost' / 'K8S_MONGO_PUBLIC_HOST'
// Sites found: 1 customer-URL emitter on the shared path
// (mongo.go Provision) + 1 on the k8s path (k8s.go, already
// correct) + admin URIs (not customer-facing).
// Sites touched: the shared emitter, via buildMongoURL — the same
// helper+publicHostPort shape as postgres.buildDBURL.
// Coverage test: TestBuildMongoURL below; the unset row pins the fallback to
// the in-cluster host (never an empty host).

import (
"strings"
"testing"
)

// mongoPublicHostEnvKeys is every env var publicHostPort consults. Tests clear
// all of them so a developer's ambient shell env cannot perturb the "unset"
// rows. A new source added to publicHostPort must be added here.
var mongoPublicHostEnvKeys = []string{
"MONGO_PUBLIC_HOST_PORT",
"MONGO_PUBLIC_HOST",
"MONGO_PUBLIC_PORT",
"K8S_MONGO_PUBLIC_HOST",
}

func clearMongoPublicHostEnv(t *testing.T) {
t.Helper()
for _, k := range mongoPublicHostEnvKeys {
t.Setenv(k, "")
}
}

// TestPublicHostPort_Mongo exercises every resolution branch of the helper.
func TestPublicHostPort_Mongo(t *testing.T) {
tests := []struct {
name string
env map[string]string
want string
}{
{
name: "nothing set — empty so the caller falls back to the admin host",
env: map[string]string{},
want: "",
},
{
name: "MONGO_PUBLIC_HOST_PORT wins over everything",
env: map[string]string{
"MONGO_PUBLIC_HOST_PORT": "mongo.instanode.dev:27020",
"MONGO_PUBLIC_HOST": "ignored.example.com",
"MONGO_PUBLIC_PORT": "1111",
"K8S_MONGO_PUBLIC_HOST": "also-ignored.example.com",
},
want: "mongo.instanode.dev:27020",
},
{
name: "MONGO_PUBLIC_HOST with the default port",
env: map[string]string{"MONGO_PUBLIC_HOST": "mongo.instanode.dev"},
want: "mongo.instanode.dev:" + defaultMongoPort,
},
{
name: "MONGO_PUBLIC_HOST with an explicit port",
env: map[string]string{
"MONGO_PUBLIC_HOST": "mongo.instanode.dev",
"MONGO_PUBLIC_PORT": "27099",
},
want: "mongo.instanode.dev:27099",
},
{
name: "MONGO_PUBLIC_HOST wins over K8S_MONGO_PUBLIC_HOST",
env: map[string]string{
"MONGO_PUBLIC_HOST": "explicit.example.com",
"K8S_MONGO_PUBLIC_HOST": "k8s.example.com",
},
want: "explicit.example.com:" + defaultMongoPort,
},
{
// The env the cluster ALREADY sets. Honouring it is what makes the
// fix a pure code change with no ops change.
name: "K8S_MONGO_PUBLIC_HOST alone — the already-configured prod env",
env: map[string]string{"K8S_MONGO_PUBLIC_HOST": "mongo.instanode.dev"},
want: "mongo.instanode.dev:" + defaultMongoPort,
},
{
name: "K8S_MONGO_PUBLIC_HOST with an explicit port",
env: map[string]string{
"K8S_MONGO_PUBLIC_HOST": "mongo.instanode.dev",
"MONGO_PUBLIC_PORT": "27098",
},
want: "mongo.instanode.dev:27098",
},
{
name: "port set but no host — still empty (a port alone addresses nothing)",
env: map[string]string{"MONGO_PUBLIC_PORT": "27099"},
want: "",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
clearMongoPublicHostEnv(t)
for k, v := range tc.env {
t.Setenv(k, v)
}
if got := publicHostPort(); got != tc.want {
t.Errorf("publicHostPort() = %q; want %q", got, tc.want)
}
})
}
}

// TestBuildMongoURL asserts the customer URL uses the public host when one is
// configured and the in-cluster admin host otherwise — never an empty host.
func TestBuildMongoURL(t *testing.T) {
const (
clusterHost = "mongodb.instant-data.svc.cluster.local:27017"
user = "usr_abc"
pass = "pw123"
db = "db_abc"
)

tests := []struct {
name string
env map[string]string
want string
}{
{
name: "public host unset — falls back to the cluster host, NOT an empty host",
env: map[string]string{},
want: "mongodb://usr_abc:pw123@" + clusterHost + "/db_abc?authSource=admin",
},
{
name: "public host set via K8S_MONGO_PUBLIC_HOST (prod today)",
env: map[string]string{"K8S_MONGO_PUBLIC_HOST": "mongo.instanode.dev"},
want: "mongodb://usr_abc:pw123@mongo.instanode.dev:27017/db_abc?authSource=admin",
},
{
name: "public host set via MONGO_PUBLIC_HOST_PORT",
env: map[string]string{"MONGO_PUBLIC_HOST_PORT": "mongo.instanode.dev:27020"},
want: "mongodb://usr_abc:pw123@mongo.instanode.dev:27020/db_abc?authSource=admin",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
clearMongoPublicHostEnv(t)
for k, v := range tc.env {
t.Setenv(k, v)
}
got := buildMongoURL(clusterHost, user, pass, db)
if got != tc.want {
t.Errorf("buildMongoURL() = %q; want %q", got, tc.want)
}
if strings.Contains(got, "@/") {
t.Errorf("buildMongoURL() = %q has an empty host", got)
}
})
}
}

// TestBuildMongoURL_NeverLeaksClusterDNSWhenPublicHostSet is the regression pin:
// with the public host configured, the internal service DNS must be gone from
// the customer's connection string entirely.
func TestBuildMongoURL_NeverLeaksClusterDNSWhenPublicHostSet(t *testing.T) {
clearMongoPublicHostEnv(t)
t.Setenv("K8S_MONGO_PUBLIC_HOST", "mongo.instanode.dev")

got := buildMongoURL("mongodb.instant-data.svc.cluster.local:27017", "usr_x", "pw", "db_x")
if strings.Contains(got, "svc.cluster.local") {
t.Errorf("customer URL still contains internal cluster DNS: %q", got)
}
}
68 changes: 67 additions & 1 deletion internal/backend/queue/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,72 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"time"
)

// natsClientPort is the NATS client-protocol port embedded in customer URLs.
// Matches the port the k8s backend hardcodes in its own customer URLs (k8s.go)
// and the port the nats-proxy listens on.
const natsClientPort = "4222"

// buildNATSURL constructs the user-facing NATS URL. Mirrors
// postgres.buildDBURL / mongo.buildMongoURL: the public host wins when
// configured, otherwise clusterHost — the in-cluster admin address, which is
// only resolvable from inside the cluster. clusterHost carries no port (config
// NATS_HOST is a bare hostname), so the client port is appended.
//
// This is the fix for the leak of internal cluster DNS into customer
// connection strings: before it, /queue/new handed out
// nats://nats.instant-data.svc.cluster.local:4222 on the shared backend,
// because the public host was applied only in the "k8s" branch of NewBackend
// and the cluster runs QUEUE_PROVISION_BACKEND=local.
func buildNATSURL(clusterHost string) string {
host := publicHostPort()
if host == "" {
host = clusterHost + ":" + natsClientPort
}
return "nats://" + host
}

// publicHostPort returns the host:port to embed in user-facing NATS URLs, or ""
// when no public host is configured (the caller then falls back to the
// cluster-internal natsHost).
//
// Identical mechanism to postgres.publicHostPort (backend/postgres/local.go),
// redis.publicHostPort (backend/redis/local.go) and mongo.publicHostPort
// (backend/mongo/mongo.go) — env-resolved at Provision time so the shared/local
// backend and the dedicated k8s backend agree on the customer-facing hostname.
//
// Resolution order:
// 1. NATS_PUBLIC_HOST_PORT (e.g. "nats.instanode.dev:4222")
// 2. NATS_PUBLIC_HOST + NATS_PUBLIC_PORT (port defaults to 4222)
// 3. K8S_NATS_PUBLIC_HOST + NATS_PUBLIC_PORT — the env the k8s branch of
// NewBackend already reads, so a cluster that already advertises
// nats.instanode.dev for dedicated pods advertises it for shared ones too.
// 4. "" — caller falls back to the in-cluster natsHost.
//
// Deliberately NO built-in default (the k8s branch defaults to
// "nats.instanode.dev"): a dev box running the shared backend against localhost
// must keep emitting localhost, not a production hostname.
func publicHostPort() string {
if hp := os.Getenv("NATS_PUBLIC_HOST_PORT"); hp != "" {
return hp
}
host := os.Getenv("NATS_PUBLIC_HOST")
if host == "" {
host = os.Getenv("K8S_NATS_PUBLIC_HOST")
}
if host == "" {
return ""
}
port := os.Getenv("NATS_PUBLIC_PORT")
if port == "" {
port = natsClientPort
}
return host + ":" + port
}

// LocalBackend provisions NATS on the shared cluster.
type LocalBackend struct {
natsHost string
Expand Down Expand Up @@ -63,7 +126,10 @@ func (b *LocalBackend) Provision(ctx context.Context, token, tier string) (*Cred

slog.Info("queue.local.provisioned", "token", token, "subject_prefix", prefix)
return &Credentials{
URL: fmt.Sprintf("nats://%s:4222", b.natsHost),
// The health check above deliberately keeps using b.natsHost: the
// monitor port is cluster-internal. Only the customer-facing URL is
// rewritten to the public host.
URL: buildNATSURL(b.natsHost),
SubjectPrefix: prefix,
}, nil
}
Expand Down
Loading
Loading