Skip to content

Commit 69cb5bf

Browse files
committed
refactor: make ssh port stable if already in .ssh/config
1 parent abee73c commit 69cb5bf

4 files changed

Lines changed: 199 additions & 37 deletions

File tree

pkg/devspace/services/proxycommands/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ func startProxyCommands(ctx *devspacecontext.Context, devContainer *latest.DevCo
5151
}
5252

5353
// get a local port
54-
port, err := ssh.LockPort()
54+
port, err := ssh.GetInstance(ctx.Log).LockPort()
5555
if err != nil {
5656
return errors.Wrap(err, "find port")
5757
}
5858

59-
defer ssh.ReleasePort(port)
59+
defer ssh.GetInstance(ctx.Log).ReleasePort(port)
6060

6161
// get remote port
6262
defaultRemotePort := DefaultRemotePort

pkg/devspace/services/ssh/config.go

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ import (
99
"io/ioutil"
1010
"os"
1111
"path/filepath"
12+
"strconv"
1213
"strings"
1314
"sync"
1415
)
1516

1617
var configLock sync.Mutex
1718

19+
var (
20+
MarkerStartPrefix = "# DevSpace Start "
21+
MarkerEndPrefix = "# DevSpace End "
22+
)
23+
1824
func configureSSHConfig(host, port string, log log.Logger) error {
1925
configLock.Lock()
2026
defer configLock.Unlock()
@@ -25,7 +31,7 @@ func configureSSHConfig(host, port string, log log.Logger) error {
2531
}
2632

2733
sshConfigPath := filepath.Join(homeDir, ".ssh", "config")
28-
newFile, err := replaceHost(sshConfigPath, host, port)
34+
newFile, err := addHost(sshConfigPath, host, port)
2935
if err != nil {
3036
return errors.Wrap(err, "parse ssh config")
3137
}
@@ -43,7 +49,65 @@ func configureSSHConfig(host, port string, log log.Logger) error {
4349
return nil
4450
}
4551

46-
func replaceHost(path, host, port string) (string, error) {
52+
type DevSpaceSSHEntry struct {
53+
Host string
54+
Hostname string
55+
Port int
56+
}
57+
58+
func ParseDevSpaceHosts(path string) ([]DevSpaceSSHEntry, error) {
59+
var reader io.Reader
60+
f, err := os.Open(path)
61+
if err != nil {
62+
if !os.IsNotExist(err) {
63+
return nil, err
64+
}
65+
66+
reader = strings.NewReader("")
67+
} else {
68+
reader = f
69+
defer f.Close()
70+
}
71+
72+
configScanner := scanner.NewScanner(reader)
73+
inSection := false
74+
75+
entries := []DevSpaceSSHEntry{}
76+
current := &DevSpaceSSHEntry{}
77+
for configScanner.Scan() {
78+
text := strings.TrimSpace(configScanner.Text())
79+
if strings.HasPrefix(text, MarkerStartPrefix) {
80+
inSection = true
81+
} else if strings.HasPrefix(text, MarkerEndPrefix) {
82+
if current.Host != "" && current.Port > 0 && current.Hostname != "" {
83+
entries = append(entries, *current)
84+
}
85+
current = &DevSpaceSSHEntry{}
86+
inSection = false
87+
} else if inSection {
88+
if strings.HasPrefix(text, "Host ") {
89+
current.Host = strings.TrimPrefix(text, "Host ")
90+
}
91+
if strings.HasPrefix(text, "Port ") {
92+
port := strings.TrimPrefix(text, "Port ")
93+
intPort, err := strconv.Atoi(port)
94+
if err == nil {
95+
current.Port = intPort
96+
}
97+
}
98+
if strings.HasPrefix(text, "HostName ") {
99+
current.Hostname = strings.TrimPrefix(text, "HostName ")
100+
}
101+
}
102+
}
103+
if configScanner.Err() != nil {
104+
return nil, errors.Wrap(err, "parse ssh config")
105+
}
106+
107+
return entries, nil
108+
}
109+
110+
func addHost(path, host, port string) (string, error) {
47111
var reader io.Reader
48112
f, err := os.Open(path)
49113
if err != nil {
@@ -77,7 +141,6 @@ func replaceHost(path, host, port string) (string, error) {
77141
}
78142

79143
// add new section
80-
newLines = append(newLines, "")
81144
newLines = append(newLines, startMarker)
82145
newLines = append(newLines, "Host "+host)
83146
newLines = append(newLines, " HostName localhost")
@@ -87,7 +150,5 @@ func replaceHost(path, host, port string) (string, error) {
87150
newLines = append(newLines, " UserKnownHostsFile /dev/null")
88151
newLines = append(newLines, " User devspace")
89152
newLines = append(newLines, endMarker)
90-
newLines = append(newLines, "")
91-
92153
return strings.Join(newLines, "\n"), nil
93154
}

pkg/devspace/services/ssh/port.go

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,114 @@ package ssh
33
import (
44
"fmt"
55
"github.com/loft-sh/devspace/helper/util/port"
6+
"github.com/loft-sh/devspace/pkg/util/log"
7+
"github.com/mitchellh/go-homedir"
8+
"github.com/pkg/errors"
69
"math/rand"
10+
"path/filepath"
711
"sync"
812
)
913

14+
type PortManager interface {
15+
LockPort() (int, error)
16+
LockSpecificPort(p int) error
17+
ReleasePort(p int)
18+
}
19+
1020
var (
11-
portRangeStart = 10000
12-
portRangeEnd = 12000
13-
portMap = map[int]bool{}
14-
portMutex sync.Mutex
21+
portManager PortManager
22+
portManagerOnce sync.Once
1523
)
1624

17-
func LockPort() (int, error) {
18-
portMutex.Lock()
19-
defer portMutex.Unlock()
25+
func GetInstance(log log.Logger) PortManager {
26+
portManagerOnce.Do(func() {
27+
portManager = NewManager(log)
28+
})
29+
return portManager
30+
}
31+
32+
func NewManager(log log.Logger) PortManager {
33+
homeDir, err := homedir.Dir()
34+
if err != nil {
35+
log.Errorf("%v", errors.Wrap(err, "get home dir"))
36+
}
37+
38+
sshConfigPath := filepath.Join(homeDir, ".ssh", "config")
39+
hosts, err := ParseDevSpaceHosts(sshConfigPath)
40+
if err != nil {
41+
log.Errorf("error parsing %s: %v", sshConfigPath, err)
42+
}
43+
44+
reservedPorts := map[int]bool{}
45+
for _, h := range hosts {
46+
reservedPorts[h.Port] = true
47+
}
48+
49+
return &manager{
50+
reservedPorts: reservedPorts,
51+
portRangeStart: 10000,
52+
portRangeEnd: 12000,
53+
portMap: map[int]bool{},
54+
}
55+
}
56+
57+
type manager struct {
58+
m sync.Mutex
59+
60+
reservedPorts map[int]bool
61+
62+
portRangeStart int
63+
portRangeEnd int
64+
portMap map[int]bool
65+
}
66+
67+
func (m *manager) LockSpecificPort(p int) error {
68+
m.m.Lock()
69+
defer m.m.Unlock()
70+
71+
if m.portMap[p] {
72+
return fmt.Errorf("port %d already in use", p)
73+
}
74+
75+
available, err := port.IsAvailable(fmt.Sprintf(":%d", p))
76+
if err != nil {
77+
return err
78+
} else if !available {
79+
return fmt.Errorf("port %d is already in use %v", p, err)
80+
}
81+
82+
m.portMap[p] = true
83+
return nil
84+
}
85+
86+
func (m *manager) LockPort() (int, error) {
87+
m.m.Lock()
88+
defer m.m.Unlock()
2089

2190
var (
2291
available bool
2392
err error
2493
)
2594
for i := 0; i < 10; i++ {
26-
p := rand.Intn(portRangeEnd-portRangeStart+1) + portRangeStart
27-
if portMap[p] {
95+
p := rand.Intn(m.portRangeEnd-m.portRangeStart+1) + m.portRangeStart
96+
if m.portMap[p] || m.reservedPorts[p] {
2897
i--
2998
continue
3099
}
31100

32101
available, err = port.IsAvailable(fmt.Sprintf(":%d", p))
33102
if available {
34-
portMap[p] = true
103+
m.portMap[p] = true
35104
return p, nil
36105
}
37106
}
38107

39108
return 0, fmt.Errorf("couldn't find an open port: %v", err)
40109
}
41110

42-
func ReleasePort(p int) {
43-
portMutex.Lock()
44-
defer portMutex.Unlock()
111+
func (m *manager) ReleasePort(p int) {
112+
m.m.Lock()
113+
defer m.m.Unlock()
45114

46-
delete(portMap, p)
115+
delete(m.portMap, p)
47116
}

pkg/devspace/services/ssh/ssh.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"bytes"
55
"fmt"
66
"github.com/mgutz/ansi"
7+
"github.com/mitchellh/go-homedir"
78
"io"
9+
"path/filepath"
810
"strconv"
911
"strings"
1012
"time"
@@ -56,24 +58,60 @@ func startSSH(ctx *devspacecontext.Context, name, arch string, sshConfig *latest
5658
return nil
5759
}
5860

59-
// get a local port
61+
// configure ssh host
62+
sshHost := name + "." + ctx.Config.Config().Name + ".devspace"
63+
if sshConfig.LocalHostname != "" {
64+
sshHost = sshConfig.LocalHostname
65+
}
66+
67+
// try to find host port
68+
homeDir, err := homedir.Dir()
69+
if err != nil {
70+
return errors.Wrap(err, "get home dir")
71+
}
72+
73+
// get port
6074
port := sshConfig.LocalPort
61-
var err error
6275
if port == 0 {
63-
port, err = LockPort()
76+
sshConfigPath := filepath.Join(homeDir, ".ssh", "config")
77+
hosts, err := ParseDevSpaceHosts(sshConfigPath)
6478
if err != nil {
65-
return errors.Wrap(err, "find port")
79+
ctx.Log.Debugf("error parsing %s: %v", sshConfigPath, err)
80+
} else {
81+
for _, h := range hosts {
82+
if h.Host == sshHost {
83+
port = h.Port
84+
}
85+
}
6686
}
6787

68-
defer ReleasePort(port)
69-
}
88+
if port == 0 {
89+
port, err = GetInstance(ctx.Log).LockPort()
90+
if err != nil {
91+
return errors.Wrap(err, "find port")
92+
}
7093

71-
// configure ssh host
72-
sshHost := name + "." + ctx.Config.Config().Name + ".devspace"
73-
if sshConfig.LocalHostname != "" {
74-
sshHost = sshConfig.LocalHostname
94+
// update ssh config
95+
err = configureSSHConfig(sshHost, strconv.Itoa(port), ctx.Log)
96+
if err != nil {
97+
return errors.Wrap(err, "update ssh config")
98+
}
99+
}
100+
} else {
101+
err = GetInstance(ctx.Log).LockSpecificPort(port)
102+
if err != nil {
103+
return errors.Wrap(err, "find port")
104+
}
105+
106+
// update ssh config
107+
err = configureSSHConfig(sshHost, strconv.Itoa(port), ctx.Log)
108+
if err != nil {
109+
return errors.Wrap(err, "update ssh config")
110+
}
75111
}
112+
defer GetInstance(ctx.Log).ReleasePort(port)
76113

114+
// get a local port
77115
// get remote port
78116
defaultRemotePort := helperssh.DefaultPort
79117
if sshConfig.RemoteAddress != "" {
@@ -99,12 +137,6 @@ func startSSH(ctx *devspacecontext.Context, name, arch string, sshConfig *latest
99137
return errors.Wrap(err, "start ssh port forwarding")
100138
}
101139

102-
// update ssh config
103-
err = configureSSHConfig(sshHost, strconv.Itoa(port), ctx.Log)
104-
if err != nil {
105-
return errors.Wrap(err, "update ssh config")
106-
}
107-
108140
// start ssh
109141
return startSSHWithRestart(ctx, arch, sshConfig.RemoteAddress, sshHost, selector, parent)
110142
}

0 commit comments

Comments
 (0)