Files
warmbly/internal/api/handler/fleet_nodes_test.go
T

177 lines
6.2 KiB
Go

package handler
import (
"strings"
"testing"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
)
// envLines turns a rendered node env into a lookup, so assertions name a
// setting rather than a line number.
func envLines(t *testing.T, rendered string) map[string]string {
t.Helper()
out := map[string]string{}
for _, line := range strings.Split(rendered, "\n") {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
t.Fatalf("line is not KEY=value: %q", line)
}
out[k] = v
}
return out
}
// setInstanceEnv puts the process in the shape of an AWS-backed control plane.
func setInstanceEnv(t *testing.T) {
t.Helper()
t.Setenv("ENCRYPTED_KEYS_BACKEND_URL", "https://api.example.com/")
t.Setenv("INTERNAL_API_TOKEN", "tok")
t.Setenv("KMS_PROVIDER", "aws")
t.Setenv("KMS_AWS_KEY_ID", "alias/warmbly")
t.Setenv("BLOB_PROVIDER", "s3")
t.Setenv("BLOB_BUCKET", "warmbly-blobs")
t.Setenv("AWS_REGION", "eu-central-1")
t.Setenv("CREDENTIALS_ENCRYPTION_KEY", "deadbeef")
t.Setenv("NATS_URL", "tls://bus.example.com:4222")
t.Setenv("REDIS", "rediss://bus.example.com:6380")
}
// A node has no cloud credential and no way to be given one, so a provider
// that needs a credential has to arrive translated. Shipping "aws" here is how
// a joined node ends up unable to open a single mailbox.
func TestRenderNodeEnvBrokersCredentialProviders(t *testing.T) {
setInstanceEnv(t)
env := envLines(t, renderNodeEnv(uuid.New(), models.NodeRoleWorker, "eu-central"))
if env["KMS_PROVIDER"] != "brokered" {
t.Errorf("KMS_PROVIDER = %q, want brokered", env["KMS_PROVIDER"])
}
if env["BLOB_PROVIDER"] != "brokered" {
t.Errorf("BLOB_PROVIDER = %q, want brokered", env["BLOB_PROVIDER"])
}
if _, ok := env["AWS_ACCESS_KEY_ID"]; ok {
t.Error("a node was handed an AWS credential")
}
}
// A provider that needs no credential works on a node as it stands, and
// translating it would break a local install for nothing.
func TestRenderNodeEnvPassesThroughLocalProviders(t *testing.T) {
setInstanceEnv(t)
t.Setenv("KMS_PROVIDER", "local")
t.Setenv("BLOB_PROVIDER", "filesystem")
t.Setenv("BLOB_FS_ROOT", "/data/blobs")
env := envLines(t, renderNodeEnv(uuid.New(), models.NodeRoleWorker, ""))
if env["KMS_PROVIDER"] != "local" {
t.Errorf("KMS_PROVIDER = %q, want local", env["KMS_PROVIDER"])
}
if env["BLOB_PROVIDER"] != "filesystem" {
t.Errorf("BLOB_PROVIDER = %q, want filesystem", env["BLOB_PROVIDER"])
}
if env["BLOB_FS_ROOT"] != "/data/blobs" {
t.Errorf("BLOB_FS_ROOT = %q", env["BLOB_FS_ROOT"])
}
}
// The regression this file exists for: every name sent has to be one the
// node's own code reads. S3_BUCKET and KMS_KEY_ID were read by nothing, so a
// node fell back to the default bucket and the default key alias in silence.
func TestRenderNodeEnvSendsNamesTheNodeReads(t *testing.T) {
setInstanceEnv(t)
t.Setenv("S3_BUCKET", "should-not-travel")
t.Setenv("KMS_KEY_ID", "should-not-travel")
env := envLines(t, renderNodeEnv(uuid.New(), models.NodeRoleWorker, ""))
for _, dead := range []string{"S3_BUCKET", "KMS_KEY_ID"} {
if _, ok := env[dead]; ok {
t.Errorf("%s is still sent; nothing reads it", dead)
}
}
if env["BLOB_BUCKET"] != "warmbly-blobs" {
t.Errorf("BLOB_BUCKET = %q, want warmbly-blobs", env["BLOB_BUCKET"])
}
if env["KMS_AWS_KEY_ID"] != "alias/warmbly" {
t.Errorf("KMS_AWS_KEY_ID = %q, want alias/warmbly", env["KMS_AWS_KEY_ID"])
}
}
// A worker reaches relational data through the internal API and nothing else.
// The DSN must not travel even when the control plane has one to send.
func TestRenderNodeEnvWithholdsDSNFromWorker(t *testing.T) {
setInstanceEnv(t)
t.Setenv("PRIMARY_DB", "postgres://u:p@db/warmbly")
env := envLines(t, renderNodeEnv(uuid.New(), models.NodeRoleWorker, ""))
if _, ok := env["PRIMARY_DB"]; ok {
t.Fatal("a worker was handed a database DSN")
}
if env["ENCRYPTED_KEYS_PROVIDER"] != "http" {
t.Errorf("ENCRYPTED_KEYS_PROVIDER = %q, want http", env["ENCRYPTED_KEYS_PROVIDER"])
}
}
// A consumer is control plane: it opens Postgres itself and cannot boot
// without the DSN, which is why joining one used to produce a node that died
// on its first start.
func TestRenderNodeEnvGivesConsumerTheDSN(t *testing.T) {
setInstanceEnv(t)
t.Setenv("PRIMARY_DB", "postgres://u:p@db/warmbly")
id := uuid.New()
env := envLines(t, renderNodeEnv(id, models.NodeRoleConsumer, ""))
if env["PRIMARY_DB"] != "postgres://u:p@db/warmbly" {
t.Errorf("PRIMARY_DB = %q", env["PRIMARY_DB"])
}
if env["ENCRYPTED_KEYS_PROVIDER"] != "postgres" {
t.Errorf("ENCRYPTED_KEYS_PROVIDER = %q, want postgres", env["ENCRYPTED_KEYS_PROVIDER"])
}
// Only a worker claims a placement identity.
if _, ok := env["WORKER_ID"]; ok {
t.Error("a consumer was given a WORKER_ID")
}
if env["WARMBLY_NODE_ID"] != id.String() {
t.Errorf("WARMBLY_NODE_ID = %q, want %s", env["WARMBLY_NODE_ID"], id)
}
}
// An instance holding its DSN in SSM has none to send. The node still has to
// be able to fetch keys, so it falls back to the HTTP path rather than being
// left with a provider it cannot satisfy.
func TestRenderNodeEnvConsumerWithoutDSNFallsBackToHTTP(t *testing.T) {
setInstanceEnv(t)
t.Setenv("PRIMARY_DB", "")
env := envLines(t, renderNodeEnv(uuid.New(), models.NodeRoleConsumer, ""))
if _, ok := env["PRIMARY_DB"]; ok {
t.Error("PRIMARY_DB was sent as an empty value")
}
if env["ENCRYPTED_KEYS_PROVIDER"] != "http" {
t.Errorf("ENCRYPTED_KEYS_PROVIDER = %q, want http", env["ENCRYPTED_KEYS_PROVIDER"])
}
}
// The worker's identity and the node row have to be the same machine.
func TestRenderNodeEnvWorkerIdentity(t *testing.T) {
setInstanceEnv(t)
id := uuid.New()
env := envLines(t, renderNodeEnv(id, models.NodeRoleWorker, "eu-central"))
if env["WORKER_ID"] != id.String() {
t.Errorf("WORKER_ID = %q, want %s", env["WORKER_ID"], id)
}
if env["WARMBLY_NODE_REGION"] != "eu-central" {
t.Errorf("WARMBLY_NODE_REGION = %q", env["WARMBLY_NODE_REGION"])
}
// The trailing slash on the instance URL must not survive into a base URL
// the node concatenates paths onto.
if env["ENCRYPTED_KEYS_BACKEND_URL"] != "https://api.example.com" {
t.Errorf("ENCRYPTED_KEYS_BACKEND_URL = %q", env["ENCRYPTED_KEYS_BACKEND_URL"])
}
}