node add (and server add, mirror peer, patroni init) is usually given an address, while the ssh config names the machine: `Host gpu01` with `HostName 10.0.0.21` and the User, IdentityFile or ProxyJump it needs. That block did not match the address, so the address had to be added to its Host line by hand. Now the concrete Host names in the config and its Includes are read, ssh -G says each one's HostName, and the first that is the address (or resolves to it) is connected through. Under sudo, the caller's name is now a fallback: a User in the config's block for the machine comes first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
2.7 KiB
Go
88 lines
2.7 KiB
Go
package onboard
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// withSSHConfig points configAlias and ssh -G at one file, and nothing else.
|
|
func withSSHConfig(t *testing.T, config string, extra map[string]string) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("ssh"); err != nil {
|
|
t.Skip("no ssh")
|
|
}
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config")
|
|
if err := os.WriteFile(path, []byte(config), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, body := range extra {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
oldUser, oldSys, oldArgs := userSSHConfig, systemSSHConfig, sshExtraArgs
|
|
userSSHConfig = func() string { return path }
|
|
systemSSHConfig = ""
|
|
sshExtraArgs = []string{"-F", path}
|
|
t.Cleanup(func() { userSSHConfig, systemSSHConfig, sshExtraArgs = oldUser, oldSys, oldArgs })
|
|
}
|
|
|
|
// A machine given by address is reached through the Host block whose
|
|
// HostName it is, with that block's user — not the sudo caller's.
|
|
func TestSSHTargetUsesConfigAlias(t *testing.T) {
|
|
withSSHConfig(t, `
|
|
Host *.example
|
|
User nobody-wild
|
|
Host gpu01 gpu01-alt
|
|
HostName 10.9.8.7
|
|
User ops
|
|
Host gpu02
|
|
HostName=10.9.8.8
|
|
`, nil)
|
|
ctx := context.Background()
|
|
cases := []struct {
|
|
s *SSH
|
|
want string
|
|
}{
|
|
// The config's User applies by ssh itself: no fallback is added.
|
|
{&SSH{Addr: "10.9.8.7", FallbackUser: "ilker"}, "gpu01"},
|
|
{&SSH{Addr: "10.9.8.7", User: "root", FallbackUser: "ilker"}, "root@gpu01"},
|
|
{&SSH{Addr: "10.9.8.8", FallbackUser: "ilker"}, "ilker@gpu02"},
|
|
{&SSH{Addr: "gpu01", FallbackUser: "ilker"}, "gpu01"},
|
|
{&SSH{Addr: "10.9.8.9", FallbackUser: "ilker"}, "ilker@10.9.8.9"},
|
|
{&SSH{Addr: "10.9.8.9"}, "10.9.8.9"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.s.Target(ctx); got != c.want {
|
|
t.Errorf("%+v: target %q, want %q", c.s.Addr, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A block in an included file counts, as it does for ssh.
|
|
func TestSSHTargetFollowsInclude(t *testing.T) {
|
|
withSSHConfig(t, "Include nodes.conf\n", map[string]string{
|
|
"nodes.conf": "Host \"gpu03\"\n HostName 10.9.8.10\n",
|
|
})
|
|
// ssh resolves a relative Include against ~/.ssh, not -F's directory;
|
|
// the scan does the same, so point base at the test's directory.
|
|
names := hostNames(userSSHConfig(), filepath.Dir(userSSHConfig()), map[string]bool{}, 0)
|
|
if len(names) != 1 || names[0] != "gpu03" {
|
|
t.Fatalf("names %q", names)
|
|
}
|
|
}
|
|
|
|
func TestConfigUserIgnoresLocalDefault(t *testing.T) {
|
|
withSSHConfig(t, "Host x\n HostName 10.9.8.11\n", nil)
|
|
if cur, err := user.Current(); err == nil {
|
|
if u := configUser(context.Background(), "x"); u != "" {
|
|
t.Errorf("config user %q for a block naming none (local %q)", u, cur.Username)
|
|
}
|
|
}
|
|
}
|