Apply configuration secrets atomically

This commit is contained in:
2026-08-13 00:18:20 +00:00
parent 26a681e0b1
commit 706086e3de
4 changed files with 163 additions and 21 deletions

View File

@@ -1706,9 +1706,13 @@ func TestLoadFileLoadsSecretsBeforeReturningNotifyConfig(t *testing.T) {
func TestLoadSecretsDisabledLeavesEnvironmentUnchanged(t *testing.T) {
t.Setenv("WEATHERREPORTER_DISABLED_SECRET", "original")
if err := loadSecrets(SecretsConfig{}); err != nil {
secrets, err := loadSecrets(SecretsConfig{})
if err != nil {
t.Fatalf("loadSecrets() error = %v", err)
}
if len(secrets) != 0 {
t.Fatalf("staged secrets = %#v, want none", secrets)
}
if got := os.Getenv("WEATHERREPORTER_DISABLED_SECRET"); got != "original" {
t.Fatalf("environment value = %q, want original", got)
}
@@ -1744,9 +1748,13 @@ func TestLoadSecretsOverwritesExistingEnvironment(t *testing.T) {
}
t.Setenv("WEATHERREPORTER_SECRET", "existing")
if err := loadSecrets(SecretsConfig{Directory: dir}); err != nil {
secrets, err := loadSecrets(SecretsConfig{Directory: dir})
if err != nil {
t.Fatalf("loadSecrets() error = %v", err)
}
if err := applySecrets(secrets); err != nil {
t.Fatalf("applySecrets() error = %v", err)
}
if got := os.Getenv("WEATHERREPORTER_SECRET"); got != "from-file" {
t.Fatalf("environment value = %q, want from-file", got)
}
@@ -1773,9 +1781,13 @@ func TestLoadSecretsTrimsOneTrailingLineEnding(t *testing.T) {
}
t.Setenv("WEATHERREPORTER_SECRET", "")
if err := loadSecrets(SecretsConfig{Directory: dir}); err != nil {
secrets, err := loadSecrets(SecretsConfig{Directory: dir})
if err != nil {
t.Fatalf("loadSecrets() error = %v", err)
}
if err := applySecrets(secrets); err != nil {
t.Fatalf("applySecrets() error = %v", err)
}
if got := os.Getenv("WEATHERREPORTER_SECRET"); got != tt.want {
t.Fatalf("environment value = %q, want %q", got, tt.want)
}
@@ -1846,7 +1858,7 @@ func TestLoadSecretsRejectsInvalidDirectoryEntries(t *testing.T) {
dir := t.TempDir()
tt.setup(t, dir)
err := loadSecrets(SecretsConfig{Directory: dir})
_, err := loadSecrets(SecretsConfig{Directory: dir})
if err == nil {
t.Fatal("loadSecrets() error = nil, want error")
}
@@ -1861,7 +1873,7 @@ func TestLoadSecretsRejectsInvalidDirectoryEntries(t *testing.T) {
}
func TestLoadSecretsRejectsMissingDirectory(t *testing.T) {
err := loadSecrets(SecretsConfig{Directory: filepath.Join(t.TempDir(), "missing")})
_, err := loadSecrets(SecretsConfig{Directory: filepath.Join(t.TempDir(), "missing")})
if err == nil {
t.Fatal("loadSecrets() error = nil, want missing directory error")
}
@@ -1869,3 +1881,82 @@ func TestLoadSecretsRejectsMissingDirectory(t *testing.T) {
t.Fatalf("error = %q, want read secrets directory context", err.Error())
}
}
func TestLoadFileSecretDirectoryFailureLeavesEnvironmentUnchanged(t *testing.T) {
dir := t.TempDir()
secretsDir := filepath.Join(dir, "secrets")
if err := os.Mkdir(secretsDir, 0o700); err != nil {
t.Fatalf("create secrets directory: %v", err)
}
if err := os.WriteFile(filepath.Join(secretsDir, "A_SECRET"), []byte("new-value"), 0o600); err != nil {
t.Fatalf("write secret: %v", err)
}
if err := os.WriteFile(filepath.Join(secretsDir, "Z-INVALID"), []byte("unused"), 0o600); err != nil {
t.Fatalf("write invalid secret: %v", err)
}
path := writeConfig(t, "secrets:\n directory: "+secretsDir+"\n")
t.Setenv("A_SECRET", "original-value")
_, err := LoadFile(path)
if err == nil || !strings.Contains(err.Error(), "invalid environment variable name") {
t.Fatalf("LoadFile() error = %v, want invalid secret filename", err)
}
if got := os.Getenv("A_SECRET"); got != "original-value" {
t.Fatalf("environment value = %q, want original-value after rejected load", got)
}
}
func TestLoadFileValidationFailureLeavesSecretEnvironmentUnset(t *testing.T) {
dir := t.TempDir()
secretsDir := filepath.Join(dir, "secrets")
if err := os.Mkdir(secretsDir, 0o700); err != nil {
t.Fatalf("create secrets directory: %v", err)
}
if err := os.WriteFile(filepath.Join(secretsDir, "WEATHERREPORTER_SECRET"), []byte("new-value"), 0o600); err != nil {
t.Fatalf("write secret: %v", err)
}
path := writeConfig(t, "secrets:\n directory: "+secretsDir+"\nmissing_source:\n default: invalid\n")
unsetEnvironment(t, "WEATHERREPORTER_SECRET")
_, err := LoadFile(path)
if err == nil || !strings.Contains(err.Error(), "missing_source.default") {
t.Fatalf("LoadFile() error = %v, want configuration validation error", err)
}
if _, set := os.LookupEnv("WEATHERREPORTER_SECRET"); set {
t.Fatal("WEATHERREPORTER_SECRET was set by a rejected configuration")
}
}
func TestApplySecretsRollsBackOnEnvironmentFailure(t *testing.T) {
t.Setenv("A_SECRET", "original-value")
unsetEnvironment(t, "Z_SECRET")
err := applySecrets([]secretValue{
{name: "A_SECRET", value: "new-value"},
{name: "Z_SECRET", value: "invalid\x00value"},
})
if err == nil || !strings.Contains(err.Error(), `secret file "Z_SECRET"`) {
t.Fatalf("applySecrets() error = %v, want Z_SECRET context", err)
}
if got := os.Getenv("A_SECRET"); got != "original-value" {
t.Fatalf("A_SECRET = %q, want original-value after rollback", got)
}
if _, set := os.LookupEnv("Z_SECRET"); set {
t.Fatal("Z_SECRET was set after failed environment application")
}
}
func unsetEnvironment(t *testing.T, name string) {
t.Helper()
value, set := os.LookupEnv(name)
if err := os.Unsetenv(name); err != nil {
t.Fatalf("unset environment variable %q: %v", name, err)
}
t.Cleanup(func() {
if set {
_ = os.Setenv(name, value)
return
}
_ = os.Unsetenv(name)
})
}