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>
184 lines
5.5 KiB
Go
184 lines
5.5 KiB
Go
package daemon
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"banger/internal/paths"
|
|
)
|
|
|
|
// 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) {
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
|
|
knownHostsPath := filepath.Join(homeDir, ".local", "state", "banger", "ssh", "known_hosts")
|
|
layout := paths.Layout{
|
|
ConfigDir: filepath.Join(homeDir, ".config", "banger"),
|
|
KnownHostsPath: knownHostsPath,
|
|
}
|
|
keyPath := filepath.Join(homeDir, ".config", "banger", "ssh", "id_ed25519")
|
|
|
|
if err := syncVMSSHClientConfig(layout, keyPath); err != nil {
|
|
t.Fatalf("syncVMSSHClientConfig: %v", err)
|
|
}
|
|
|
|
// Banger's own ssh_config file has the `Host *.vm` stanza.
|
|
bangerConfig, err := os.ReadFile(BangerSSHConfigPath(layout))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(banger ssh_config): %v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Host *.vm",
|
|
"IdentityFile " + keyPath,
|
|
"UserKnownHostsFile " + knownHostsPath,
|
|
"StrictHostKeyChecking accept-new",
|
|
} {
|
|
if !strings.Contains(string(bangerConfig), want) {
|
|
t.Fatalf("banger ssh_config missing %q:\n%s", want, bangerConfig)
|
|
}
|
|
}
|
|
|
|
// ~/.ssh/config must NOT have been created or modified.
|
|
if _, err := os.Stat(filepath.Join(homeDir, ".ssh", "config")); !os.IsNotExist(err) {
|
|
t.Fatalf("~/.ssh/config should be untouched; stat err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallUserSSHIncludeAddsIncludeBlock(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)
|
|
}
|
|
// Write a fake banger ssh_config so Install has something to include.
|
|
if err := os.WriteFile(BangerSSHConfigPath(layout), []byte("Host *.vm\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(banger ssh_config): %v", err)
|
|
}
|
|
|
|
if err := InstallUserSSHInclude(layout); err != nil {
|
|
t.Fatalf("InstallUserSSHInclude: %v", err)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "config"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(~/.ssh/config): %v", err)
|
|
}
|
|
want := "Include " + BangerSSHConfigPath(layout)
|
|
if !strings.Contains(string(got), want) {
|
|
t.Fatalf("user config missing %q:\n%s", want, got)
|
|
}
|
|
if !strings.Contains(string(got), bangerSSHIncludeBegin) {
|
|
t.Fatalf("user config missing begin marker:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestInstallUserSSHIncludeIsIdempotent(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)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if err := InstallUserSSHInclude(layout); err != nil {
|
|
t.Fatalf("InstallUserSSHInclude (%d): %v", i, err)
|
|
}
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(homeDir, ".ssh", "config"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if n := strings.Count(string(got), bangerSSHIncludeBegin); n != 1 {
|
|
t.Fatalf("begin markers = %d, want 1:\n%s", n, got)
|
|
}
|
|
}
|
|
|
|
func TestUninstallUserSSHIncludeRemovesIncludeBlock(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
|
|
sshDir := filepath.Join(homeDir, ".ssh")
|
|
if err := os.MkdirAll(sshDir, 0o700); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
seed := strings.Join([]string{
|
|
"Host keep",
|
|
" HostName 198.51.100.1",
|
|
"",
|
|
bangerSSHIncludeBegin,
|
|
"Include /tmp/banger-ssh-config",
|
|
bangerSSHIncludeEnd,
|
|
"",
|
|
}, "\n")
|
|
if err := os.WriteFile(filepath.Join(sshDir, "config"), []byte(seed), 0o600); err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
|
|
if err := UninstallUserSSHInclude(); err != nil {
|
|
t.Fatalf("UninstallUserSSHInclude: %v", err)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(sshDir, "config"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
gotStr := string(got)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestUninstallUserSSHIncludeIsNoOpWhenMissing(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
if err := UninstallUserSSHInclude(); err != nil {
|
|
t.Fatalf("UninstallUserSSHInclude on missing file: %v", err)
|
|
}
|
|
// Still no ~/.ssh/config.
|
|
if _, err := os.Stat(filepath.Join(homeDir, ".ssh", "config")); !os.IsNotExist(err) {
|
|
t.Fatalf("~/.ssh/config unexpectedly created; stat err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUserSSHIncludeInstalledDetectsMarker(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
seed string
|
|
wantIn bool
|
|
}{
|
|
{"missing file", "", false},
|
|
{"unrelated only", "Host other\n HostName 1.2.3.4\n", false},
|
|
{"installed", bangerSSHIncludeBegin + "\nInclude /tmp/banger\n" + bangerSSHIncludeEnd + "\n", true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
homeDir := t.TempDir()
|
|
t.Setenv("HOME", homeDir)
|
|
if tc.seed != "" {
|
|
if err := os.MkdirAll(filepath.Join(homeDir, ".ssh"), 0o700); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(homeDir, ".ssh", "config"), []byte(tc.seed), 0o600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
}
|
|
got, err := UserSSHIncludeInstalled()
|
|
if err != nil {
|
|
t.Fatalf("UserSSHIncludeInstalled: %v", err)
|
|
}
|
|
if got != tc.wantIn {
|
|
t.Fatalf("got %v, want %v", got, tc.wantIn)
|
|
}
|
|
})
|
|
}
|
|
}
|