This repository was archived by the owner on Jan 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 216
+727
−292
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
dist: xenial | ||
language: go | ||
|
||
go: | ||
- 1.12.x | ||
go_import_path: go.coder.com/retry | ||
env: | ||
- GO111MODULE=on | ||
script: | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"math/rand" | ||
"net" | ||
"net/http" | ||
"os" | ||
"os/exec" | ||
"os/signal" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/pkg/browser" | ||
"go.coder.com/flog" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
type options struct { | ||
skipSync bool | ||
syncBack bool | ||
localPort string | ||
remotePort string | ||
sshFlags string | ||
} | ||
|
||
func sshCode(host, dir string, o options) error { | ||
flog.Info("ensuring code-server is updated...") | ||
|
||
const codeServerPath = "/tmp/sshcode-code-server" | ||
|
||
dlScript := downloadScript(codeServerPath) | ||
|
||
// Downloads the latest code-server and allows it to be executed. | ||
sshCmdStr := fmt.Sprintf("ssh %v %v /bin/bash", o.sshFlags, host) | ||
|
||
sshCmd := exec.Command("sh", "-c", sshCmdStr) | ||
sshCmd.Stdout = os.Stdout | ||
sshCmd.Stderr = os.Stderr | ||
sshCmd.Stdin = strings.NewReader(dlScript) | ||
err := sshCmd.Run() | ||
if err != nil { | ||
return xerrors.Errorf("failed to update code-server: \n---ssh cmd---\n%s\n---download script---\n%s: %w", | ||
sshCmdStr, | ||
dlScript, | ||
err, | ||
) | ||
} | ||
|
||
if !o.skipSync { | ||
start := time.Now() | ||
flog.Info("syncing settings") | ||
err = syncUserSettings(o.sshFlags, host, false) | ||
if err != nil { | ||
return xerrors.Errorf("failed to sync settings: %w", err) | ||
} | ||
|
||
flog.Info("synced settings in %s", time.Since(start)) | ||
|
||
flog.Info("syncing extensions") | ||
err = syncExtensions(o.sshFlags, host, false) | ||
if err != nil { | ||
return xerrors.Errorf("failed to sync extensions: %w", err) | ||
} | ||
flog.Info("synced extensions in %s", time.Since(start)) | ||
} | ||
|
||
flog.Info("starting code-server...") | ||
|
||
if o.localPort == "" { | ||
o.localPort, err = randomPort() | ||
} | ||
if err != nil { | ||
return xerrors.Errorf("failed to find available local port: %w", err) | ||
} | ||
|
||
if o.remotePort == "" { | ||
o.remotePort, err = randomPort() | ||
} | ||
if err != nil { | ||
return xerrors.Errorf("failed to find available remote port: %w", err) | ||
} | ||
|
||
flog.Info("Tunneling local port %v to remote port %v", o.localPort, o.remotePort) | ||
|
||
sshCmdStr = fmt.Sprintf("ssh -tt -q -L %v %v %v 'cd %v; %v --host 127.0.0.1 --allow-http --no-auth --port=%v'", | ||
o.localPort+":localhost:"+o.remotePort, o.sshFlags, host, dir, codeServerPath, o.remotePort, | ||
) | ||
|
||
// Starts code-server and forwards the remote port. | ||
sshCmd = exec.Command("sh", "-c", sshCmdStr) | ||
sshCmd.Stdin = os.Stdin | ||
sshCmd.Stdout = os.Stdout | ||
sshCmd.Stderr = os.Stderr | ||
err = sshCmd.Start() | ||
if err != nil { | ||
flog.Fatal("failed to start code-server: %v", err) | ||
} | ||
|
||
url := "http://127.0.0.1:" + o.localPort | ||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) | ||
defer cancel() | ||
|
||
client := http.Client{ | ||
Timeout: time.Second * 3, | ||
} | ||
for { | ||
if ctx.Err() != nil { | ||
return xerrors.Errorf("code-server didn't start in time: %w", ctx.Err()) | ||
} | ||
// Waits for code-server to be available before opening the browser. | ||
resp, err := client.Get(url) | ||
if err != nil { | ||
continue | ||
} | ||
resp.Body.Close() | ||
break | ||
} | ||
|
||
ctx, cancel = context.WithCancel(context.Background()) | ||
|
||
if os.Getenv("DISPLAY") != "" { | ||
openBrowser(url) | ||
} | ||
|
||
go func() { | ||
defer cancel() | ||
sshCmd.Wait() | ||
}() | ||
|
||
c := make(chan os.Signal) | ||
signal.Notify(c, os.Interrupt) | ||
|
||
select { | ||
case <-ctx.Done(): | ||
case <-c: | ||
} | ||
|
||
if !o.syncBack || o.skipSync { | ||
flog.Info("shutting down") | ||
return nil | ||
} | ||
|
||
flog.Info("synchronizing VS Code back to local") | ||
|
||
err = syncExtensions(o.sshFlags, host, true) | ||
if err != nil { | ||
return xerrors.Errorf("failed to sync extensions back: %w", err) | ||
} | ||
|
||
err = syncUserSettings(o.sshFlags, host, true) | ||
if err != nil { | ||
return xerrors.Errorf("failed to sync user settings settings back: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func openBrowser(url string) { | ||
var openCmd *exec.Cmd | ||
|
||
const ( | ||
macPath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | ||
wslPath = "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" | ||
) | ||
|
||
switch { | ||
case commandExists("google-chrome"): | ||
openCmd = exec.Command("google-chrome", chromeOptions(url)...) | ||
case commandExists("google-chrome-stable"): | ||
openCmd = exec.Command("google-chrome-stable", chromeOptions(url)...) | ||
case commandExists("chromium"): | ||
openCmd = exec.Command("chromium", chromeOptions(url)...) | ||
case commandExists("chromium-browser"): | ||
openCmd = exec.Command("chromium-browser", chromeOptions(url)...) | ||
case pathExists(macPath): | ||
openCmd = exec.Command(macPath, chromeOptions(url)...) | ||
case pathExists(wslPath): | ||
openCmd = exec.Command(wslPath, chromeOptions(url)...) | ||
default: | ||
err := browser.OpenURL(url) | ||
if err != nil { | ||
flog.Error("failed to open browser: %v", err) | ||
} | ||
return | ||
} | ||
|
||
// We do not use CombinedOutput because if there is no chrome instance, this will block | ||
// and become the parent process instead of using an existing chrome instance. | ||
err := openCmd.Start() | ||
if err != nil { | ||
flog.Error("failed to open browser: %v", err) | ||
} | ||
} | ||
|
||
func chromeOptions(url string) []string { | ||
return []string{"--app=" + url, "--disable-extensions", "--disable-plugins", "--incognito"} | ||
} | ||
|
||
// Checks if a command exists locally. | ||
func commandExists(name string) bool { | ||
_, err := exec.LookPath(name) | ||
return err == nil | ||
} | ||
|
||
func pathExists(name string) bool { | ||
_, err := os.Stat(name) | ||
return err == nil | ||
} | ||
|
||
// randomPort picks a random port to start code-server on. | ||
func randomPort() (string, error) { | ||
const ( | ||
minPort = 1024 | ||
maxPort = 65535 | ||
maxTries = 10 | ||
) | ||
for i := 0; i < maxTries; i++ { | ||
port := rand.Intn(maxPort-minPort+1) + minPort | ||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) | ||
if err == nil { | ||
_ = l.Close() | ||
return strconv.Itoa(port), nil | ||
} | ||
flog.Info("port taken: %d", port) | ||
} | ||
|
||
return "", xerrors.Errorf("max number of tries exceeded: %d", maxTries) | ||
} | ||
|
||
func syncUserSettings(sshFlags string, host string, back bool) error { | ||
localConfDir, err := configDir() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = ensureDir(localConfDir) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
const remoteSettingsDir = "~/.local/share/code-server/User/" | ||
|
||
var ( | ||
src = localConfDir + "/" | ||
dest = host + ":" + remoteSettingsDir | ||
) | ||
|
||
if back { | ||
dest, src = src, dest | ||
} | ||
|
||
// Append "/" to have rsync copy the contents of the dir. | ||
return rsync(src, dest, sshFlags, "workspaceStorage", "logs", "CachedData") | ||
} | ||
|
||
func syncExtensions(sshFlags string, host string, back bool) error { | ||
localExtensionsDir, err := extensionsDir() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = ensureDir(localExtensionsDir) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
const remoteExtensionsDir = "~/.local/share/code-server/extensions/" | ||
|
||
var ( | ||
src = localExtensionsDir + "/" | ||
dest = host + ":" + remoteExtensionsDir | ||
) | ||
if back { | ||
dest, src = src, dest | ||
} | ||
|
||
return rsync(src, dest, sshFlags) | ||
} | ||
|
||
func rsync(src string, dest string, sshFlags string, excludePaths ...string) error { | ||
excludeFlags := make([]string, len(excludePaths)) | ||
for i, path := range excludePaths { | ||
excludeFlags[i] = "--exclude=" + path | ||
} | ||
|
||
cmd := exec.Command("rsync", append(excludeFlags, "-azvr", | ||
"-e", "ssh "+sshFlags, | ||
// Only update newer directories, and sync times | ||
// to keep things simple. | ||
"-u", "--times", | ||
// This is more unsafe, but it's obnoxious having to enter VS Code | ||
// locally in order to properly delete an extension. | ||
"--delete", | ||
"--copy-unsafe-links", | ||
src, dest, | ||
)..., | ||
) | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
err := cmd.Run() | ||
if err != nil { | ||
return xerrors.Errorf("failed to rsync '%s' to '%s': %w", src, dest, err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func downloadScript(codeServerPath string) string { | ||
return fmt.Sprintf( | ||
`set -euxo pipefail || exit 1 | ||
mkdir -p ~/.local/share/code-server | ||
cd %v | ||
wget -N https://codesrv-ci.cdr.sh/latest-linux | ||
[ -f %v ] && rm %v | ||
ln latest-linux %v | ||
chmod +x %v`, | ||
filepath.Dir(codeServerPath), | ||
codeServerPath, | ||
codeServerPath, | ||
codeServerPath, | ||
codeServerPath, | ||
) | ||
} | ||
|
||
// ensureDir creates a directory if it does not exist. | ||
func ensureDir(path string) error { | ||
_, err := os.Stat(path) | ||
if os.IsNotExist(err) { | ||
err = os.MkdirAll(path, 0750) | ||
} | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
"os" | ||
"os/exec" | ||
"strconv" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
"go.coder.com/retry" | ||
"golang.org/x/crypto/ssh" | ||
) | ||
|
||
func TestSSHCode(t *testing.T) { | ||
// Avoid opening a browser window. | ||
err := os.Unsetenv("DISPLAY") | ||
require.NoError(t, err) | ||
|
||
sshPort, err := randomPort() | ||
require.NoError(t, err) | ||
|
||
// start up our jank ssh server | ||
defer trassh(t, sshPort).Close() | ||
|
||
localPort := randomPortExclude(t, sshPort) | ||
require.NotEmpty(t, localPort) | ||
|
||
remotePort := randomPortExclude(t, sshPort, localPort) | ||
require.NotEmpty(t, remotePort) | ||
|
||
var wg sync.WaitGroup | ||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
err := sshCode("127.0.0.1", "", options{ | ||
sshFlags: testSSHArgs(sshPort), | ||
localPort: localPort, | ||
remotePort: remotePort, | ||
}) | ||
require.NoError(t, err) | ||
}() | ||
|
||
waitForSSHCode(t, localPort, time.Second*30) | ||
waitForSSHCode(t, remotePort, time.Second*30) | ||
|
||
out, err := exec.Command("pkill", "sshcode-code").CombinedOutput() | ||
require.NoError(t, err, "%s", out) | ||
|
||
wg.Wait() | ||
} | ||
|
||
// trassh is an incomplete, local, insecure ssh server | ||
// used for the purpose of testing the implementation without | ||
// requiring the user to have their own remote server. | ||
func trassh(t *testing.T, port string) io.Closer { | ||
private, err := ssh.ParsePrivateKey([]byte(fakeRSAKey)) | ||
require.NoError(t, err) | ||
|
||
conf := &ssh.ServerConfig{ | ||
NoClientAuth: true, | ||
} | ||
|
||
conf.AddHostKey(private) | ||
|
||
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", port)) | ||
require.NoError(t, err) | ||
|
||
go func() { | ||
for { | ||
func() { | ||
conn, err := listener.Accept() | ||
if err != nil { | ||
return | ||
} | ||
defer conn.Close() | ||
|
||
sshConn, chans, reqs, err := ssh.NewServerConn(conn, conf) | ||
require.NoError(t, err) | ||
|
||
go ssh.DiscardRequests(reqs) | ||
|
||
for c := range chans { | ||
switch c.ChannelType() { | ||
case "direct-tcpip": | ||
var req directTCPIPReq | ||
|
||
err := ssh.Unmarshal(c.ExtraData(), &req) | ||
if err != nil { | ||
t.Logf("failed to unmarshal tcpip data: %v", err) | ||
continue | ||
} | ||
|
||
ch, _, err := c.Accept() | ||
if err != nil { | ||
c.Reject(ssh.ConnectionFailed, fmt.Sprintf("unable to accept channel: %v", err)) | ||
continue | ||
} | ||
|
||
go handleDirectTCPIP(ch, &req, t) | ||
case "session": | ||
ch, inReqs, err := c.Accept() | ||
if err != nil { | ||
c.Reject(ssh.ConnectionFailed, fmt.Sprintf("unable to accept channel: %v", err)) | ||
continue | ||
} | ||
|
||
go handleSession(ch, inReqs, t) | ||
default: | ||
t.Logf("unsupported session type: %v\n", c.ChannelType()) | ||
c.Reject(ssh.UnknownChannelType, "unknown channel type") | ||
} | ||
} | ||
|
||
sshConn.Wait() | ||
}() | ||
} | ||
}() | ||
return listener | ||
} | ||
|
||
func handleDirectTCPIP(ch ssh.Channel, req *directTCPIPReq, t *testing.T) { | ||
defer ch.Close() | ||
|
||
dstAddr := net.JoinHostPort(req.Host, strconv.Itoa(int(req.Port))) | ||
|
||
conn, err := net.Dial("tcp", dstAddr) | ||
if err != nil { | ||
return | ||
} | ||
defer conn.Close() | ||
|
||
var wg sync.WaitGroup | ||
|
||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
defer ch.Close() | ||
|
||
io.Copy(ch, conn) | ||
}() | ||
|
||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
defer conn.Close() | ||
|
||
io.Copy(conn, ch) | ||
}() | ||
wg.Wait() | ||
} | ||
|
||
// execReq describes an exec payload. | ||
type execReq struct { | ||
Command string | ||
} | ||
|
||
// directTCPIPReq describes the extra data sent in a | ||
// direct-tcpip request containing the host/port for the ssh server. | ||
type directTCPIPReq struct { | ||
Host string | ||
Port uint32 | ||
|
||
Orig string | ||
OrigPort uint32 | ||
} | ||
|
||
// exitStatus describes an 'exit-status' message | ||
// returned after a request. | ||
type exitStatus struct { | ||
Status uint32 | ||
} | ||
|
||
func handleSession(ch ssh.Channel, in <-chan *ssh.Request, t *testing.T) { | ||
defer ch.Close() | ||
|
||
for req := range in { | ||
if req.WantReply { | ||
req.Reply(true, nil) | ||
} | ||
|
||
// TODO support the rest of the types e.g. env, pty, etc. | ||
// Right now they aren't necessary for the tests. | ||
if req.Type != "exec" { | ||
t.Logf("Unsupported session type %v, only 'exec' is supported", req.Type) | ||
continue | ||
} | ||
|
||
var exReq execReq | ||
err := ssh.Unmarshal(req.Payload, &exReq) | ||
if err != nil { | ||
t.Logf("failed to unmarshal exec payload %s", req.Payload) | ||
return | ||
} | ||
|
||
cmd := exec.Command("sh", "-c", exReq.Command) | ||
|
||
stdin, err := cmd.StdinPipe() | ||
require.NoError(t, err) | ||
|
||
go func() { | ||
defer stdin.Close() | ||
io.Copy(stdin, ch) | ||
}() | ||
|
||
cmd.Stdout = ch | ||
cmd.Stderr = ch.Stderr() | ||
err = cmd.Run() | ||
|
||
var exit exitStatus | ||
if err != nil { | ||
exErr, ok := err.(*exec.ExitError) | ||
require.True(t, ok, "Not an exec.ExitError, was %T", err) | ||
|
||
exit.Status = uint32(exErr.ExitCode()) | ||
} | ||
|
||
_, err = ch.SendRequest("exit-status", false, ssh.Marshal(&exit)) | ||
if err != nil { | ||
t.Logf("unable to send status: %v", err) | ||
} | ||
break | ||
} | ||
} | ||
|
||
func waitForSSHCode(t *testing.T, port string, timeout time.Duration) { | ||
var ( | ||
url = fmt.Sprintf("http://localhost:%v/", port) | ||
client = &http.Client{ | ||
Timeout: time.Second, | ||
} | ||
) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
defer cancel() | ||
|
||
backoff := &retry.Backoff{ | ||
Floor: time.Second, | ||
Ceil: time.Second, | ||
} | ||
|
||
for { | ||
resp, err := client.Get(url) | ||
if err == nil { | ||
require.Equal(t, http.StatusOK, resp.StatusCode) | ||
return | ||
} | ||
err = backoff.Wait(ctx) | ||
require.NoError(t, err) | ||
} | ||
} | ||
|
||
// fakeRSAKey isn't used for anything other than the trashh ssh | ||
// server. | ||
const fakeRSAKey = `-----BEGIN RSA PRIVATE KEY----- | ||
MIIEpQIBAAKCAQEAsbbGAxPQeqti2OgdzuMgJGBAwXe/bFhQTPuk0bIvavkZwX/a | ||
NhmXV0dhLino5KtjR8oEazLxOgnOkJ6mpwVEgUhNMZhD9jEHZ7at4DtBIwfxjHjv | ||
nF+kJAt4xX4AZYbwIfLN9TsDGGhv4wPlB7mbwv+lhmPK+HsLbajO4n69k3s0WW94 | ||
LafJntx/98o9gL2R7hpbMxgUu8cSZjYakkRBQdab0xUuTiceq0HfAOBCQpEw0meF | ||
cmhMeeu7H5UwKGj573pBxON0G1SJgipkcs4TD2rZ9wjc29gDJjHjf3Ko/JzX1WFL | ||
db21fzqRGWelgCHCUsIvUBeExk4jM1d63JrmFQIDAQABAoIBAQCdc9OSjG6tEMYe | ||
aeFnGQK0V/dnskIOq1xSKK7J/7ZVb+iq8S0Tu67D7IEklos6dsMaqtkpZVQm2OOE | ||
bJw45MjiRn3mUAL+0EfAUzFQtw8qC3Kuw8N/55kVOnjBeba+PUTqvyZNfQBsErP3 | ||
Dc9Q/dkMdtZf8HC3oMTqXqMWN7adQBQRBspUBkLQeSemYsUm2cc+YSnCwKel98uN | ||
EuDJaTZwutxTUF1FBoXlejYlVKcldk1w5HtKkjGdW+mbo2xUpu8W0620Rs/fXNpU | ||
+guAlpB1/Wx5foZqZx33Ul8HINfDre/uqHwCd+ucDIyV7TfIh9JV5w3iRLa0QCz0 | ||
kFe/GsEtAoGBAODRa1GwfyK+gcgxF2qwfsxF3I+DQhqWFiCA0o5kO2fpiUR3rDQj | ||
XhBoPxr/qYBSBtGErHIiB7WFeQ6GjVTEgY/cEkIIh1tY95UWQ3/oIZWW498dQGRh | ||
SUGXm3lMrSsVCyXxNexSH5yTrRzyZ2u4mZupMeyACoGRGkNTVppOU4XbAoGBAMpc | ||
1ifX3kr5m8CXa6mI+NWFAQlhW0Ak0hjhM/WDzMrSimYxLLSkaKyUSHnFP/8V4asA | ||
tV173lVut2Cjv5v5FcrOnI33Li2IcNlOzCRiLHzZ43HXckcoQDcU8iKTBq1a0Dx1 | ||
eXr2rs+a/2pTy7IMsxyJVCSP6IDBI9+2iW+Cxh7PAoGBAMOa0hJAS02yjX7d367v | ||
I1OeETo4jQJOxa/ABfLoGJvfoJQWv5iZkRUbbpSSDytbsx0Gn3eqTiTMnbhar4sq | ||
ckP1yVj0zLhY3wkzVsVp9haOM3ODouvzjWZpf1d5tE2AwLNhfHZCOcjk4EEIU51w | ||
/w1ll89a1ElJM52SXA5jyd3zAoGBAKGtpKi2rvMGFKu+DxWnyu+FUXu2HhrUkEuy | ||
ejn5MMEHj+3v8gDtrnfcDT/FGclrKR7f9QeYtN1bFQYQLkGmtAOSKcC/MVTNwyPL | ||
8gxLp7GkwDSvZq11ekDH6mE3SMluWhtD3Ggi+S4Db3f7NS6vONde3SxNEfz00v2l | ||
MI84U6Q/AoGAVTZGT5weqRTJSqnri6Noz+5j/73QMf/QiZDgHMMCF0giC2mxqOgR | ||
QF6+cxHQe0sbMQ/xJU5RYhgnqSa2TjLMju4N2nQ9i/HqI/3p0CPwjFsZWlXmWEK9 | ||
5kdld52W7Bu2vQuFbg2Oy7aPhnI+1CqlubOFRgMe4AJND2t9SMTV+rc= | ||
-----END RSA PRIVATE KEY----- | ||
` | ||
|
||
func testSSHArgs(port string) string { | ||
return "-o StrictHostKeyChecking=no -p " + port | ||
} | ||
|
||
func randomPortExclude(t *testing.T, exludedPorts ...string) string { | ||
valid := func(port string) bool { | ||
for _, exPort := range exludedPorts { | ||
if exPort == port { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
maxTries := 10 | ||
for i := 0; i < maxTries; i++ { | ||
port, err := randomPort() | ||
require.NoError(t, err) | ||
|
||
if valid(port) { | ||
return port | ||
} | ||
} | ||
|
||
return "" | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.