mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
281 lines
9.4 KiB
Go
281 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
|
|
)
|
|
|
|
func TestWriteHandoff_WritesEveryFileRootOnly(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "handoff")
|
|
password := "s3cret"
|
|
|
|
err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
|
Mode: handlers_rds.BootstrapModeInitialize,
|
|
Engine: "postgres",
|
|
MasterUsername: "master",
|
|
ListenAddress: "10.0.1.20",
|
|
ClientCIDR: "10.0.0.0/16",
|
|
MasterUserPassword: &password,
|
|
DBName: "appdb",
|
|
Port: 5433,
|
|
DataVolumeID: "vol-data-01",
|
|
DataVolumeSerial: "voldata01",
|
|
VMGeneration: 4,
|
|
FormatAuthorized: true,
|
|
Parameters: []handlers_rds.Parameter{
|
|
{Name: "max_connections", Value: "200"},
|
|
{Name: "shared_buffers", Value: "256MB"},
|
|
},
|
|
ServingCertificate: "CERT",
|
|
ServingPrivateKey: "KEY",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("writeHandoff: %v", err)
|
|
}
|
|
|
|
env := readFile(t, filepath.Join(dir, handoffEnvFile))
|
|
for _, want := range []string{
|
|
"RDS_MODE='initialize'",
|
|
"RDS_MASTER_USERNAME='master'",
|
|
"RDS_MASTER_PASSWORD='s3cret'",
|
|
"RDS_DB_NAME='appdb'",
|
|
"RDS_PORT='5433'",
|
|
"RDS_DATA_VOLUME_ID='vol-data-01'",
|
|
"RDS_DATA_VOLUME_SERIAL='voldata01'",
|
|
"RDS_VM_GENERATION='4'",
|
|
"RDS_FORMAT_AUTHORIZED='true'",
|
|
} {
|
|
if !strings.Contains(env, want) {
|
|
t.Errorf("handoff env missing %s:\n%s", want, env)
|
|
}
|
|
}
|
|
|
|
params := readFile(t, filepath.Join(dir, handoffParamsFile))
|
|
if !strings.Contains(params, "max_connections = '200'") || !strings.Contains(params, "shared_buffers = '256MB'") {
|
|
t.Errorf("parameters.conf = %q, want both resolved settings", params)
|
|
}
|
|
if got := readFile(t, filepath.Join(dir, handoffCertFile)); got != "CERT" {
|
|
t.Errorf("server.crt = %q, want CERT", got)
|
|
}
|
|
if got := readFile(t, filepath.Join(dir, handoffKeyFile)); got != "KEY" {
|
|
t.Errorf("server.key = %q, want KEY", got)
|
|
}
|
|
|
|
// The handoff holds a master password and a private key; anything readable
|
|
// beyond root is a disclosure to every process in the guest.
|
|
if mode := statMode(t, dir); mode != handoffDirMode {
|
|
t.Errorf("handoff dir mode = %#o, want %#o", mode, handoffDirMode)
|
|
}
|
|
for _, name := range []string{handoffEnvFile, handoffParamsFile, handoffCertFile, handoffKeyFile} {
|
|
if mode := statMode(t, filepath.Join(dir, name)); mode != handoffMode {
|
|
t.Errorf("%s mode = %#o, want %#o", name, mode, handoffMode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Attach mode carries no password, and the key must be absent rather than
|
|
// present and empty — rds-init reads an empty value as "no password" only by
|
|
// accident, and a later boot would inherit it.
|
|
func TestWriteHandoff_AttachOmitsPassword(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "handoff")
|
|
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
|
Mode: handlers_rds.BootstrapModeAttach,
|
|
Engine: "postgres",
|
|
MasterUsername: "master",
|
|
ListenAddress: "10.0.1.20",
|
|
ClientCIDR: "10.0.0.0/16",
|
|
Port: 5432,
|
|
}); err != nil {
|
|
t.Fatalf("writeHandoff: %v", err)
|
|
}
|
|
|
|
env := readFile(t, filepath.Join(dir, handoffEnvFile))
|
|
if strings.Contains(env, "RDS_MASTER_PASSWORD") {
|
|
t.Errorf("handoff env = %q, want no RDS_MASTER_PASSWORD key", env)
|
|
}
|
|
for _, want := range []string{
|
|
"RDS_DATA_VOLUME_ID=''",
|
|
"RDS_DATA_VOLUME_SERIAL=''",
|
|
"RDS_VM_GENERATION='0'",
|
|
"RDS_FORMAT_AUTHORIZED='false'",
|
|
} {
|
|
if !strings.Contains(env, want) {
|
|
t.Errorf("handoff env missing fail-closed field %s: %q", want, env)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The env file is sourced by a shell, so a password holding shell metacharacters
|
|
// must survive as data rather than being split, expanded or executed.
|
|
func TestWriteHandoff_QuotesShellMetacharacters(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "handoff")
|
|
password := `a b$(id);'"\` + "`whoami`"
|
|
|
|
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
|
Mode: handlers_rds.BootstrapModeInitialize,
|
|
Engine: "postgres",
|
|
MasterUsername: "master",
|
|
ListenAddress: "10.0.1.20",
|
|
ClientCIDR: "10.0.0.0/16",
|
|
MasterUserPassword: &password,
|
|
Port: 5432,
|
|
}); err != nil {
|
|
t.Fatalf("writeHandoff: %v", err)
|
|
}
|
|
|
|
env := readFile(t, filepath.Join(dir, handoffEnvFile))
|
|
line := findLine(t, env, "RDS_MASTER_PASSWORD=")
|
|
if got := shellSource(t, line, "RDS_MASTER_PASSWORD"); got != password {
|
|
t.Errorf("sourced password = %q, want %q (line was %s)", got, password, line)
|
|
}
|
|
}
|
|
|
|
// A serving cert with no key would have rds-init configure TLS against a key
|
|
// that is not there; the pair is delivered together or not at all.
|
|
func TestWriteHandoff_SkipsHalfATLSPair(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "handoff")
|
|
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
|
Mode: handlers_rds.BootstrapModeAttach,
|
|
Engine: "postgres",
|
|
MasterUsername: "master",
|
|
ListenAddress: "10.0.1.20",
|
|
ClientCIDR: "10.0.0.0/16",
|
|
Port: 5432,
|
|
ServingCertificate: "CERT",
|
|
}); err != nil {
|
|
t.Fatalf("writeHandoff: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(dir, handoffCertFile)); !os.IsNotExist(err) {
|
|
t.Errorf("server.crt exists without a key; stat err = %v", err)
|
|
}
|
|
}
|
|
|
|
// A config with no master username cannot bootstrap anything, and writing it
|
|
// would only move the failure into rds-init after the boot has committed to it.
|
|
func TestWriteHandoff_RejectsConfigWithNoMasterUser(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "handoff")
|
|
err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
|
Mode: handlers_rds.BootstrapModeAttach,
|
|
Engine: "postgres",
|
|
Port: 5432,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("writeHandoff accepted a config with no master username, want an error")
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(dir, handoffEnvFile)); !os.IsNotExist(statErr) {
|
|
t.Errorf("handoff env was written despite the rejection; stat err = %v", statErr)
|
|
}
|
|
}
|
|
|
|
// A directory left over from an earlier boot at a looser mode must be tightened,
|
|
// not accepted as it is found.
|
|
func TestWriteHandoff_TightensExistingDirectory(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "handoff")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatalf("seed dir: %v", err)
|
|
}
|
|
|
|
if err := writeHandoff(dir, &handlers_rds.GetDBBootstrapConfigOutput{
|
|
Mode: handlers_rds.BootstrapModeAttach,
|
|
Engine: "postgres",
|
|
MasterUsername: "master",
|
|
ListenAddress: "10.0.1.20",
|
|
ClientCIDR: "10.0.0.0/16",
|
|
Port: 5432,
|
|
}); err != nil {
|
|
t.Fatalf("writeHandoff: %v", err)
|
|
}
|
|
if mode := statMode(t, dir); mode != handoffDirMode {
|
|
t.Errorf("handoff dir mode = %#o, want it tightened to %#o", mode, handoffDirMode)
|
|
}
|
|
}
|
|
|
|
func TestShellQuote_EscapesEmbeddedQuotes(t *testing.T) {
|
|
for _, in := range []string{"", "plain", "it's", `''`, "new\nline", `$HOME`} {
|
|
if got := shellSource(t, "V="+shellQuote(in), "V"); got != in {
|
|
t.Errorf("shellQuote(%q) sourced back as %q", in, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// shellSource evaluates one KEY=value assignment in a real shell and returns
|
|
// what the variable ends up holding. The quoting is only correct if a shell
|
|
// agrees, so the test asks one instead of re-implementing its rules.
|
|
func shellSource(t *testing.T, line, name string) string {
|
|
t.Helper()
|
|
out, err := exec.Command("/bin/sh", "-c", line+"\nprintf %s \"$"+name+"\"\n").Output()
|
|
if err != nil {
|
|
t.Fatalf("sourcing %q: %v", line, err)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func statMode(t *testing.T, path string) os.FileMode {
|
|
t.Helper()
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stat %s: %v", path, err)
|
|
}
|
|
return info.Mode().Perm()
|
|
}
|
|
|
|
func findLine(t *testing.T, body, prefix string) string {
|
|
t.Helper()
|
|
for line := range strings.SplitSeq(body, "\n") {
|
|
if strings.HasPrefix(line, prefix) {
|
|
return line
|
|
}
|
|
}
|
|
t.Fatalf("no line starting %q in:\n%s", prefix, body)
|
|
return ""
|
|
}
|
|
|
|
// The include rds-init installs is what the engine starts on, so the spelling
|
|
// written here is the one that has to boot.
|
|
func TestRenderParameters_UsesTheEngineStartupSpelling(t *testing.T) {
|
|
params := []handlers_rds.Parameter{
|
|
{Name: "time_zone", Value: "SYSTEM"},
|
|
{Name: "max_connections", Value: "85"},
|
|
}
|
|
|
|
rendered, err := renderParameters("mariadb", params)
|
|
if err != nil {
|
|
t.Fatalf("renderParameters: %v", err)
|
|
}
|
|
if !strings.Contains(rendered, "default_time_zone = 'SYSTEM'\n") {
|
|
t.Errorf("time_zone was not written under its startup spelling:\n%s", rendered)
|
|
}
|
|
if strings.Contains(rendered, "\ntime_zone = ") {
|
|
t.Errorf("time_zone reached the option file, which mariadbd refuses:\n%s", rendered)
|
|
}
|
|
if !strings.Contains(rendered, "max_connections = '85'\n") {
|
|
t.Errorf("an unmapped parameter was not written unchanged:\n%s", rendered)
|
|
}
|
|
}
|
|
|
|
func TestRenderParameters_PostgresKeepsEveryName(t *testing.T) {
|
|
rendered, err := renderParameters("postgres", []handlers_rds.Parameter{
|
|
{Name: "shared_buffers", Value: "262144"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("renderParameters: %v", err)
|
|
}
|
|
if !strings.Contains(rendered, "shared_buffers = '262144'\n") {
|
|
t.Errorf("postgres name was rewritten:\n%s", rendered)
|
|
}
|
|
}
|
|
|
|
// Failing closed beats writing a file the engine may refuse: the bad include
|
|
// would already be on the data volume by the time the server rejected it.
|
|
func TestRenderParameters_RefusesAnUnknownEngine(t *testing.T) {
|
|
if _, err := renderParameters("", nil); err == nil {
|
|
t.Fatal("renderParameters accepted a config naming no engine, want an error")
|
|
}
|
|
}
|