From c1cdcb8c85ffb4ea86f7a88e2467cdd4c01ee8dc Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Sun, 15 Feb 2026 20:15:34 -0800 Subject: [PATCH] store.Inspect: fall back to ssh.config for SSH port When the host agent is unavailable, the SSH local port remains 0 because Inspect can only retrieve it from the host agent. Extract the port from the instance's ssh.config file as a fallback; this file persists after the host agent dies and contains the port assigned at startup. Fixes #4583 Signed-off-by: Jan Dubois --- pkg/store/instance.go | 23 +++++++++++++++++++++++ pkg/store/instance_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/pkg/store/instance.go b/pkg/store/instance.go index 59d520f7cdc..2c704f784bb 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -87,6 +87,14 @@ func Inspect(ctx context.Context, instName string) (*limatype.Instance, error) { } } } + if inst.SSHLocalPort == 0 { + sshConfigPath := filepath.Join(instDir, filenames.SSHConfig) + if port, err := sshPortFromConfig(sshConfigPath); err == nil { + inst.SSHLocalPort = port + } else if !errors.Is(err, os.ErrNotExist) { + inst.Errors = append(inst.Errors, fmt.Errorf("failed to extract SSH local port from %q: %w", sshConfigPath, err)) + } + } inst.CPUs = *y.CPUs memory, err := units.RAMInBytes(*y.Memory) @@ -226,6 +234,21 @@ func ReadPIDFile(path string) (int, error) { return pid, nil } +// sshPortFromConfig extracts the SSH port from an ssh config file. +func sshPortFromConfig(configPath string) (int, error) { + b, err := os.ReadFile(configPath) + if err != nil { + return 0, err + } + for line := range strings.SplitSeq(string(b), "\n") { + line = strings.TrimSpace(line) + if port, ok := strings.CutPrefix(line, "Port "); ok { + return strconv.Atoi(port) + } + } + return 0, fmt.Errorf("port not found in %q", configPath) +} + type FormatData struct { limatype.Instance `yaml:",inline"` diff --git a/pkg/store/instance_test.go b/pkg/store/instance_test.go index 28ffcb7aa8d..0a3da8abc56 100644 --- a/pkg/store/instance_test.go +++ b/pkg/store/instance_test.go @@ -66,6 +66,33 @@ var tableTwo = "NAME STATUS SSH VMTYPE ARCH CPUS M "foo Stopped 127.0.0.1:0 qemu x86_64 0 0B 0B\n" + "bar Stopped 127.0.0.1:0 vz aarch64 0 0B 0B\n" +func TestSSHPortFromConfig(t *testing.T) { + t.Run("valid", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "ssh.config") + content := "Host lima-default\n Hostname 127.0.0.1\n Port 58786\n User foo\n" + assert.NilError(t, os.WriteFile(f, []byte(content), 0o644)) + port, err := sshPortFromConfig(f) + assert.NilError(t, err) + assert.Equal(t, 58786, port) + }) + t.Run("missing file", func(t *testing.T) { + _, err := sshPortFromConfig(filepath.Join(t.TempDir(), "nonexistent")) + assert.ErrorIs(t, err, os.ErrNotExist) + }) + t.Run("no port line", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "ssh.config") + assert.NilError(t, os.WriteFile(f, []byte("Host lima-default\n Hostname 127.0.0.1\n"), 0o644)) + _, err := sshPortFromConfig(f) + assert.ErrorContains(t, err, "port not found") + }) + t.Run("invalid port", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "ssh.config") + assert.NilError(t, os.WriteFile(f, []byte("Host lima-default\n Port abc\n"), 0o644)) + _, err := sshPortFromConfig(f) + assert.ErrorContains(t, err, "invalid syntax") + }) +} + func TestPrintInstanceTable(t *testing.T) { var buf bytes.Buffer instances := []*limatype.Instance{&instance}