cleanup: drop pre-v0.1 migration scaffolding + legacy-behavior refs
banger hasn't shipped a public release — every "legacy", "pre-opt-in",
"previously", "migration note", "no longer" reference in the tree is
pinning against a state no real user's install has ever been in.
That scaffolding has weight: it's a coordinate system future readers
have to decode, and it keeps dead code alive.
Removed (code):
- internal/daemon/ssh_client_config.go
- vmSSHConfigIncludeBegin / vmSSHConfigIncludeEnd constants and
every `removeManagedBlock(existing, vm...)` call they enabled
(legacy inline `Host *.vm` block scrub)
- cleanupLegacySSHConfigDir (+ its caller in syncVMSSHClientConfig)
— wiped a pre-opt-in sibling file under $ConfigDir/ssh
- sameDirOrParent + resolvePathForComparison — only ever used
by cleanupLegacySSHConfigDir
- the "also check legacy marker" fallback in
UserSSHIncludeInstalled / UninstallUserSSHInclude
- internal/store/migrations.go
- migrateDropDeadImageColumns (migration 2) + its slice entry
- dropColumnIfExists (orphaned after the above)
- addColumnIfMissing + the whole "columns added across the pre-
versioning lifetime" block at the end of migrateBaseline —
subsumed into the baseline CREATE TABLE
- `packages_path TEXT` column on the images table (the
throwaway migration 2 dropped it, but there was never any
reader)
- internal/daemon/vm.go
- vmDNSRecordName local wrapper — was justified as "avoid
pulling vmdns into every file"; three of four callers already
imported vmdns directly, so inline the one stray call
- internal/cli/cli_test.go
- TestLegacyRemovedCommandIsRejected (`tui` subcommand never
shipped)
Removed / simplified (tests):
- ssh_client_config_test.go: dropped TestSameDirOrParentHandlesSymlinks,
TestSyncVMSSHClientConfigPreservesUserKeyInLegacyDir,
TestSyncVMSSHClientConfigNarrowsCleanupToLegacyFile,
TestSyncVMSSHClientConfigLeavesUnexpectedLegacyContents,
TestInstallUserSSHIncludeMigratesLegacyInlineBlock, plus the
"legacy posture" regression strings in the remaining happy-path
test; TestUninstallUserSSHIncludeRemovesBothMarkerBlocks collapsed
to a single-block test
- migrations_test.go: dropped TestMigrateDropDeadImageColumns_AcrossInstallPaths,
TestDropColumnIfExistsIsIdempotent; TestOpenReadOnlyDoesNotRunMigrations
simplified to test against the baseline marker
Removed (docs):
- README.md "**Migration note.**" blockquote about the SSH-key path move
- docs/advanced.md parenthetical "(the old behaviour)"
Reworded (comments):
- Dropped "Previously this file also contained LogLevel DEBUG3..."
history from vm_disk.go's sshdGuestConfig doc
- Dropped "Call sites that previously read vm.Runtime.{PID,...}"
from vm_handles.go; now documents the current contract
- Dropped "Pre-v0.1 the defaults are" scaffolding in doctor_test.go
- Dropped "no longer does its own git inspection" phrasing in vm_run.go
- Dropped the "(also cleans up legacy inline block from pre-opt-in
builds)" aside on the `ssh-config` CLI docstring
- Renamed test var `legacyKey` → `existingKey` in vm_test.go; its
purpose was "pre-existing authorized_keys line," not banger-legacy
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5791466498
commit
700a1e6e60
16 changed files with 54 additions and 735 deletions
|
|
@ -9,200 +9,6 @@ import (
|
|||
"banger/internal/paths"
|
||||
)
|
||||
|
||||
// TestSameDirOrParentHandlesSymlinks guards against a drift where
|
||||
// sameDirOrParent (the gate that protects a user key under the
|
||||
// legacy dir from the cleanup scrub) compares lexical paths and
|
||||
// misses symlink aliasing.
|
||||
//
|
||||
// Scenario: user configured ssh_key_path at a path that lands inside
|
||||
// ConfigDir/ssh via a symlink (e.g. ConfigDir is itself symlinked,
|
||||
// or the user maintains a symlink alias for their key tree). The
|
||||
// gate must resolve both sides to the same physical location and
|
||||
// refuse to scrub.
|
||||
func TestSameDirOrParentHandlesSymlinks(t *testing.T) {
|
||||
physical := t.TempDir()
|
||||
realDir := filepath.Join(physical, "real-ssh")
|
||||
if err := os.Mkdir(realDir, 0o700); err != nil {
|
||||
t.Fatalf("Mkdir: %v", err)
|
||||
}
|
||||
realKey := filepath.Join(realDir, "id_ed25519")
|
||||
if err := os.WriteFile(realKey, []byte("PRIVATE"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
// A symlink that aliases the whole real-ssh directory. The user
|
||||
// configured ssh_key_path via this alias, but sameDirOrParent is
|
||||
// called with the canonical (realDir) legacyDir path.
|
||||
aliasDir := filepath.Join(physical, "alias-ssh")
|
||||
if err := os.Symlink(realDir, aliasDir); err != nil {
|
||||
t.Skipf("symlink unsupported on this filesystem: %v", err)
|
||||
}
|
||||
aliasKey := filepath.Join(aliasDir, "id_ed25519")
|
||||
|
||||
if !sameDirOrParent(realDir, aliasKey) {
|
||||
t.Fatalf("sameDirOrParent(%q, %q) = false; symlinked key was not recognised as inside the dir — cleanup would delete it", realDir, aliasKey)
|
||||
}
|
||||
|
||||
// Reverse direction: dir provided as a symlink, key as canonical.
|
||||
if !sameDirOrParent(aliasDir, realKey) {
|
||||
t.Fatalf("sameDirOrParent(%q, %q) = false; reverse symlink direction also missed", aliasDir, realKey)
|
||||
}
|
||||
|
||||
// Negative: a key in a completely unrelated directory must not
|
||||
// be reported inside either spelling of the legacy dir.
|
||||
outside := filepath.Join(t.TempDir(), "other", "id_ed25519")
|
||||
if err := os.MkdirAll(filepath.Dir(outside), 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(outside, []byte("UNRELATED"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
if sameDirOrParent(realDir, outside) {
|
||||
t.Fatalf("sameDirOrParent(%q, %q) = true; unrelated dir incorrectly flagged as inside", realDir, outside)
|
||||
}
|
||||
}
|
||||
|
||||
// A user-configured ssh_key_path that happens to live under the
|
||||
// legacy $ConfigDir/ssh directory must survive the pre-opt-in
|
||||
// migration cleanup. The old code did os.RemoveAll on the whole
|
||||
// directory, which nuked the key. Pin the narrower behavior so a
|
||||
// future refactor can't re-broaden the scrub.
|
||||
func TestSyncVMSSHClientConfigPreservesUserKeyInLegacyDir(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "banger")
|
||||
legacyDir := filepath.Join(configDir, "ssh")
|
||||
if err := os.MkdirAll(legacyDir, 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
userKey := filepath.Join(legacyDir, "id_ed25519")
|
||||
if err := os.WriteFile(userKey, []byte("PRIVATE"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
// A stale ssh_config under the same dir from pre-opt-in era.
|
||||
legacyConfig := filepath.Join(legacyDir, "ssh_config")
|
||||
if err := os.WriteFile(legacyConfig, []byte("stale"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
layout := paths.Layout{
|
||||
ConfigDir: configDir,
|
||||
KnownHostsPath: filepath.Join(homeDir, ".local", "state", "banger", "ssh", "known_hosts"),
|
||||
}
|
||||
if err := syncVMSSHClientConfig(layout, userKey); err != nil {
|
||||
t.Fatalf("syncVMSSHClientConfig: %v", err)
|
||||
}
|
||||
|
||||
// The configured key must survive.
|
||||
if _, err := os.Stat(userKey); err != nil {
|
||||
t.Fatalf("user-configured key disappeared: %v", err)
|
||||
}
|
||||
// Enclosing directory must also survive (it contains the key).
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
t.Fatalf("legacy dir removed despite containing the configured key: %v", err)
|
||||
}
|
||||
// The stale legacy ssh_config file can still be gone in this
|
||||
// case — the user's key isn't ssh_config, so cleaning up the
|
||||
// sibling file is fine. We don't assert either way, since the
|
||||
// gate is "don't delete the user's key" not "always delete the
|
||||
// sibling file."
|
||||
}
|
||||
|
||||
// With ssh_key_path configured outside ConfigDir/ssh, the legacy
|
||||
// migration step should scrub the old sibling file and then the
|
||||
// (now-empty) directory — no os.RemoveAll on anything still in use.
|
||||
func TestSyncVMSSHClientConfigNarrowsCleanupToLegacyFile(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "banger")
|
||||
legacyDir := filepath.Join(configDir, "ssh")
|
||||
if err := os.MkdirAll(legacyDir, 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
// Simulate the pre-opt-in leftover: just the ssh_config file.
|
||||
legacyConfig := filepath.Join(legacyDir, "ssh_config")
|
||||
if err := os.WriteFile(legacyConfig, []byte("stale"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
// ssh_key_path lives in the state dir (the new default location).
|
||||
stateDir := filepath.Join(homeDir, ".local", "state", "banger", "ssh")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
userKey := filepath.Join(stateDir, "id_ed25519")
|
||||
if err := os.WriteFile(userKey, []byte("PRIVATE"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
layout := paths.Layout{
|
||||
ConfigDir: configDir,
|
||||
KnownHostsPath: filepath.Join(homeDir, ".local", "state", "banger", "ssh", "known_hosts"),
|
||||
}
|
||||
if err := syncVMSSHClientConfig(layout, userKey); err != nil {
|
||||
t.Fatalf("syncVMSSHClientConfig: %v", err)
|
||||
}
|
||||
|
||||
// Legacy ssh_config file: gone.
|
||||
if _, err := os.Stat(legacyConfig); !os.IsNotExist(err) {
|
||||
t.Fatalf("legacy ssh_config survived cleanup: %v", err)
|
||||
}
|
||||
// Legacy dir: gone, since it was empty after the file removal.
|
||||
if _, err := os.Stat(legacyDir); !os.IsNotExist(err) {
|
||||
t.Fatalf("legacy dir survived cleanup when empty: %v", err)
|
||||
}
|
||||
// User's key: untouched.
|
||||
if _, err := os.Stat(userKey); err != nil {
|
||||
t.Fatalf("user key disappeared: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If the legacy dir contains UNEXPECTED files (not ssh_config, not
|
||||
// the configured key), leave the dir alone. os.Remove on a non-
|
||||
// empty dir errors with ENOTEMPTY, which we swallow. Regression
|
||||
// guard so the cleanup can never escalate to recursive deletion.
|
||||
func TestSyncVMSSHClientConfigLeavesUnexpectedLegacyContents(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "banger")
|
||||
legacyDir := filepath.Join(configDir, "ssh")
|
||||
if err := os.MkdirAll(legacyDir, 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
// A user-managed file we have no business removing.
|
||||
userFile := filepath.Join(legacyDir, "my-other-thing")
|
||||
if err := os.WriteFile(userFile, []byte("mine"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
layout := paths.Layout{
|
||||
ConfigDir: configDir,
|
||||
KnownHostsPath: filepath.Join(homeDir, ".local", "state", "banger", "ssh", "known_hosts"),
|
||||
}
|
||||
// ssh_key_path lives elsewhere; cleanup would otherwise proceed.
|
||||
stateKey := filepath.Join(homeDir, ".local", "state", "banger", "ssh", "id_ed25519")
|
||||
if err := os.MkdirAll(filepath.Dir(stateKey), 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(stateKey, []byte("PRIVATE"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
if err := syncVMSSHClientConfig(layout, stateKey); err != nil {
|
||||
t.Fatalf("syncVMSSHClientConfig: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(userFile); err != nil {
|
||||
t.Fatalf("user-managed legacy-dir file disappeared: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
t.Fatalf("legacy dir vanished despite non-empty contents: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Under the opt-in contract the daemon writes its own ssh_config file
|
||||
// and never touches ~/.ssh/config on its own.
|
||||
func TestSyncVMSSHClientConfigWritesBangerFileOnly(t *testing.T) {
|
||||
|
|
@ -240,17 +46,6 @@ func TestSyncVMSSHClientConfigWritesBangerFileOnly(t *testing.T) {
|
|||
if _, err := os.Stat(filepath.Join(homeDir, ".ssh", "config")); !os.IsNotExist(err) {
|
||||
t.Fatalf("~/.ssh/config should be untouched; stat err = %v", err)
|
||||
}
|
||||
|
||||
// Regression: the legacy posture (strict no + /dev/null) must not
|
||||
// reappear in the banger file.
|
||||
for _, must := range []string{
|
||||
"StrictHostKeyChecking no",
|
||||
"UserKnownHostsFile /dev/null",
|
||||
} {
|
||||
if strings.Contains(string(bangerConfig), must) {
|
||||
t.Fatalf("banger ssh_config leaked legacy posture %q:\n%s", must, bangerConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallUserSSHIncludeAddsIncludeBlock(t *testing.T) {
|
||||
|
|
@ -307,64 +102,7 @@ func TestInstallUserSSHIncludeIsIdempotent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInstallUserSSHIncludeMigratesLegacyInlineBlock(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
layout := paths.Layout{ConfigDir: filepath.Join(homeDir, ".config", "banger")}
|
||||
if err := os.MkdirAll(layout.ConfigDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(BangerSSHConfigPath(layout), []byte("Host *.vm\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
sshDir := filepath.Join(homeDir, ".ssh")
|
||||
if err := os.MkdirAll(sshDir, 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll(.ssh): %v", err)
|
||||
}
|
||||
legacy := strings.Join([]string{
|
||||
"ServerAliveInterval 120",
|
||||
"",
|
||||
vmSSHConfigIncludeBegin,
|
||||
"Host *.vm",
|
||||
" User root",
|
||||
" IdentityFile /some/old/key",
|
||||
vmSSHConfigIncludeEnd,
|
||||
"",
|
||||
"Host other",
|
||||
" HostName 192.0.2.5",
|
||||
"",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(filepath.Join(sshDir, "config"), []byte(legacy), 0o600); err != nil {
|
||||
t.Fatalf("seed legacy config: %v", err)
|
||||
}
|
||||
|
||||
if err := InstallUserSSHInclude(layout); err != nil {
|
||||
t.Fatalf("InstallUserSSHInclude: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(sshDir, "config"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
gotStr := string(got)
|
||||
// Legacy inline block must be gone.
|
||||
if strings.Contains(gotStr, vmSSHConfigIncludeBegin) {
|
||||
t.Fatalf("legacy inline block survived:\n%s", gotStr)
|
||||
}
|
||||
// New Include block must be present.
|
||||
if !strings.Contains(gotStr, bangerSSHIncludeBegin) {
|
||||
t.Fatalf("new include block missing:\n%s", gotStr)
|
||||
}
|
||||
// Unrelated stanzas must be preserved.
|
||||
for _, want := range []string{"ServerAliveInterval 120", "Host other"} {
|
||||
if !strings.Contains(gotStr, want) {
|
||||
t.Fatalf("user config lost unrelated entry %q:\n%s", want, gotStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallUserSSHIncludeRemovesBothMarkerBlocks(t *testing.T) {
|
||||
func TestUninstallUserSSHIncludeRemovesIncludeBlock(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
|
|
@ -376,10 +114,6 @@ func TestUninstallUserSSHIncludeRemovesBothMarkerBlocks(t *testing.T) {
|
|||
"Host keep",
|
||||
" HostName 198.51.100.1",
|
||||
"",
|
||||
vmSSHConfigIncludeBegin,
|
||||
"Host *.vm",
|
||||
vmSSHConfigIncludeEnd,
|
||||
"",
|
||||
bangerSSHIncludeBegin,
|
||||
"Include /tmp/banger-ssh-config",
|
||||
bangerSSHIncludeEnd,
|
||||
|
|
@ -397,10 +131,8 @@ func TestUninstallUserSSHIncludeRemovesBothMarkerBlocks(t *testing.T) {
|
|||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
gotStr := string(got)
|
||||
for _, banned := range []string{vmSSHConfigIncludeBegin, bangerSSHIncludeBegin} {
|
||||
if strings.Contains(gotStr, banned) {
|
||||
t.Fatalf("residue of %q:\n%s", banned, gotStr)
|
||||
}
|
||||
if strings.Contains(gotStr, bangerSSHIncludeBegin) {
|
||||
t.Fatalf("begin marker survived uninstall:\n%s", gotStr)
|
||||
}
|
||||
if !strings.Contains(gotStr, "Host keep") {
|
||||
t.Fatalf("lost unrelated entry:\n%s", gotStr)
|
||||
|
|
@ -419,7 +151,7 @@ func TestUninstallUserSSHIncludeIsNoOpWhenMissing(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUserSSHIncludeInstalledDetectsBothMarkers(t *testing.T) {
|
||||
func TestUserSSHIncludeInstalledDetectsMarker(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
seed string
|
||||
|
|
@ -427,8 +159,7 @@ func TestUserSSHIncludeInstalledDetectsBothMarkers(t *testing.T) {
|
|||
}{
|
||||
{"missing file", "", false},
|
||||
{"unrelated only", "Host other\n HostName 1.2.3.4\n", false},
|
||||
{"legacy marker", vmSSHConfigIncludeBegin + "\nHost *.vm\n" + vmSSHConfigIncludeEnd + "\n", true},
|
||||
{"new marker", bangerSSHIncludeBegin + "\nInclude /tmp/banger\n" + bangerSSHIncludeEnd + "\n", true},
|
||||
{"installed", bangerSSHIncludeBegin + "\nInclude /tmp/banger\n" + bangerSSHIncludeEnd + "\n", true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue