cloud/shepherd: move ssh client to own package

Change-Id: I56ad16f8f2f355243c5c0414656bbfbbff1faef5
Reviewed-on: https://review.monogon.dev/c/monogon/+/2791
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/shepherd/manager/BUILD.bazel b/cloud/shepherd/manager/BUILD.bazel
index 4119ff7..aa61caa 100644
--- a/cloud/shepherd/manager/BUILD.bazel
+++ b/cloud/shepherd/manager/BUILD.bazel
@@ -9,7 +9,6 @@
         "manager.go",
         "provisioner.go",
         "recoverer.go",
-        "ssh_client.go",
         "ssh_key_signer.go",
     ],
     importpath = "source.monogon.dev/cloud/shepherd/manager",
@@ -21,8 +20,8 @@
         "//cloud/bmaas/bmdb/model",
         "//cloud/shepherd",
         "//go/mflags",
+        "//go/net/ssh",
         "@com_github_google_uuid//:uuid",
-        "@com_github_pkg_sftp//:sftp",
         "@io_k8s_klog_v2//:klog",
         "@org_golang_google_protobuf//proto",
         "@org_golang_x_crypto//ssh",
@@ -47,6 +46,7 @@
         "//cloud/bmaas/bmdb/model",
         "//cloud/lib/component",
         "//cloud/shepherd",
+        "//go/net/ssh",
         "@com_github_google_uuid//:uuid",
         "@io_k8s_klog_v2//:klog",
         "@org_golang_x_time//rate",
diff --git a/cloud/shepherd/manager/fake_ssh_client.go b/cloud/shepherd/manager/fake_ssh_client.go
index 1d9d371..db13b5b 100644
--- a/cloud/shepherd/manager/fake_ssh_client.go
+++ b/cloud/shepherd/manager/fake_ssh_client.go
@@ -5,20 +5,23 @@
 	"crypto/ed25519"
 	"crypto/rand"
 	"fmt"
+	"io"
 	"time"
 
 	"google.golang.org/protobuf/proto"
 
 	apb "source.monogon.dev/cloud/agent/api"
+
+	"source.monogon.dev/go/net/ssh"
 )
 
-// FakeSSHClient is an SSHClient that pretends to start an agent, but in reality
+// FakeSSHClient is an Client that pretends to start an agent, but in reality
 // just responds with what an agent would respond on every execution attempt.
 type FakeSSHClient struct{}
 
 type fakeSSHConnection struct{}
 
-func (f *FakeSSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (SSHConnection, error) {
+func (f *FakeSSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (ssh.Connection, error) {
 	return &fakeSSHConnection{}, nil
 }
 
@@ -46,7 +49,7 @@
 	return arspb, nil, nil
 }
 
-func (f *fakeSSHConnection) Upload(ctx context.Context, targetPath string, data []byte) error {
+func (f *fakeSSHConnection) Upload(ctx context.Context, targetPath string, _ io.Reader) error {
 	if targetPath != "/fake/path" {
 		return fmt.Errorf("unexpected target path in test")
 	}
diff --git a/cloud/shepherd/manager/initializer.go b/cloud/shepherd/manager/initializer.go
index 5abbc68..2c04c43 100644
--- a/cloud/shepherd/manager/initializer.go
+++ b/cloud/shepherd/manager/initializer.go
@@ -1,6 +1,7 @@
 package manager
 
 import (
+	"bytes"
 	"context"
 	"crypto/ed25519"
 	"crypto/x509"
@@ -23,6 +24,7 @@
 	"source.monogon.dev/cloud/bmaas/bmdb/metrics"
 	"source.monogon.dev/cloud/bmaas/bmdb/model"
 	"source.monogon.dev/cloud/shepherd"
+	"source.monogon.dev/go/net/ssh"
 )
 
 // InitializerConfig configures how the Initializer will deploy Agents on
@@ -126,13 +128,13 @@
 type Initializer struct {
 	InitializerConfig
 
-	sshClient SSHClient
+	sshClient ssh.Client
 	p         shepherd.Provider
 }
 
 // NewInitializer creates an Initializer instance, checking the
 // InitializerConfig, SharedConfig and AgentConfig for errors.
-func NewInitializer(p shepherd.Provider, sshClient SSHClient, ic InitializerConfig) (*Initializer, error) {
+func NewInitializer(p shepherd.Provider, sshClient ssh.Client, ic InitializerConfig) (*Initializer, error) {
 	if err := ic.Check(); err != nil {
 		return nil, err
 	}
@@ -219,7 +221,7 @@
 	// Upload the agent executable.
 
 	klog.Infof("Uploading the agent executable (machine ID: %s, addr: %s).", mid, addr)
-	if err := conn.Upload(sctx, i.TargetPath, i.Executable); err != nil {
+	if err := conn.Upload(sctx, i.TargetPath, bytes.NewReader(i.Executable)); err != nil {
 		return nil, fmt.Errorf("while uploading agent executable: %w", err)
 	}
 	klog.V(1).Infof("Upload successful (machine ID: %s, addr: %s).", mid, addr)
diff --git a/cloud/shepherd/manager/provider_test.go b/cloud/shepherd/manager/provider_test.go
index b18dc45..4cdfb18 100644
--- a/cloud/shepherd/manager/provider_test.go
+++ b/cloud/shepherd/manager/provider_test.go
@@ -12,6 +12,7 @@
 	"source.monogon.dev/cloud/bmaas/bmdb"
 	"source.monogon.dev/cloud/bmaas/bmdb/model"
 	"source.monogon.dev/cloud/shepherd"
+	"source.monogon.dev/go/net/ssh"
 )
 
 type dummyMachine struct {
@@ -38,17 +39,17 @@
 }
 
 type dummySSHClient struct {
-	SSHClient
+	ssh.Client
 	dp *dummyProvider
 }
 
 type dummySSHConnection struct {
-	SSHConnection
+	ssh.Connection
 	m *dummyMachine
 }
 
 func (dsc *dummySSHConnection) Execute(ctx context.Context, command string, stdin []byte) ([]byte, []byte, error) {
-	stdout, stderr, err := dsc.SSHConnection.Execute(ctx, command, stdin)
+	stdout, stderr, err := dsc.Connection.Execute(ctx, command, stdin)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -57,8 +58,8 @@
 	return stdout, stderr, nil
 }
 
-func (dsc *dummySSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (SSHConnection, error) {
-	conn, err := dsc.SSHClient.Dial(ctx, address, timeout)
+func (dsc *dummySSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (ssh.Connection, error) {
+	conn, err := dsc.Client.Dial(ctx, address, timeout)
 	if err != nil {
 		return nil, err
 	}
@@ -77,10 +78,10 @@
 	return &dummySSHConnection{conn, m}, nil
 }
 
-func (dp *dummyProvider) sshClient() SSHClient {
+func (dp *dummyProvider) sshClient() ssh.Client {
 	return &dummySSHClient{
-		SSHClient: &FakeSSHClient{},
-		dp:        dp,
+		Client: &FakeSSHClient{},
+		dp:     dp,
 	}
 }
 
diff --git a/cloud/shepherd/mini/BUILD.bazel b/cloud/shepherd/mini/BUILD.bazel
index 63ad885..5fad3c2 100644
--- a/cloud/shepherd/mini/BUILD.bazel
+++ b/cloud/shepherd/mini/BUILD.bazel
@@ -16,6 +16,7 @@
         "//cloud/lib/component",
         "//cloud/shepherd",
         "//cloud/shepherd/manager",
+        "//go/net/ssh",
         "//metropolis/cli/pkg/context",
         "@io_k8s_klog_v2//:klog",
         "@org_golang_x_crypto//ssh",
diff --git a/cloud/shepherd/mini/ssh.go b/cloud/shepherd/mini/ssh.go
index 99f3e90..1a2d23c 100644
--- a/cloud/shepherd/mini/ssh.go
+++ b/cloud/shepherd/mini/ssh.go
@@ -4,10 +4,11 @@
 	"flag"
 	"fmt"
 
-	"golang.org/x/crypto/ssh"
+	xssh "golang.org/x/crypto/ssh"
 	"k8s.io/klog/v2"
 
 	"source.monogon.dev/cloud/shepherd/manager"
+	"source.monogon.dev/go/net/ssh"
 )
 
 type sshConfig struct {
@@ -36,18 +37,18 @@
 	sc.SSHKey.RegisterFlags()
 }
 
-func (sc *sshConfig) NewClient() (*manager.PlainSSHClient, error) {
+func (sc *sshConfig) NewClient() (*ssh.DirectClient, error) {
 	if err := sc.check(); err != nil {
 		return nil, err
 	}
 
-	c := manager.PlainSSHClient{
+	c := ssh.DirectClient{
 		Username: sc.User,
 	}
 
 	switch {
 	case sc.Pass != "":
-		c.AuthMethod = ssh.Password(sc.Pass)
+		c.AuthMethod = xssh.Password(sc.Pass)
 	case sc.SSHKey.KeyPersistPath != "":
 		signer, err := sc.SSHKey.Signer()
 		if err != nil {
@@ -61,7 +62,7 @@
 
 		klog.Infof("Using ssh key auth with public key: %s", pubKey)
 
-		c.AuthMethod = ssh.PublicKeys(signer)
+		c.AuthMethod = xssh.PublicKeys(signer)
 	}
 	return &c, nil
 }
diff --git a/cloud/shepherd/provider/equinix/BUILD.bazel b/cloud/shepherd/provider/equinix/BUILD.bazel
index 727fbcf..4214f16 100644
--- a/cloud/shepherd/provider/equinix/BUILD.bazel
+++ b/cloud/shepherd/provider/equinix/BUILD.bazel
@@ -20,6 +20,7 @@
         "//cloud/lib/sinbin",
         "//cloud/shepherd",
         "//cloud/shepherd/manager",
+        "//go/net/ssh",
         "//metropolis/cli/pkg/context",
         "@com_github_packethost_packngo//:packngo",
         "@io_k8s_klog_v2//:klog",
diff --git a/cloud/shepherd/provider/equinix/main.go b/cloud/shepherd/provider/equinix/main.go
index 3a402e8..0060502 100644
--- a/cloud/shepherd/provider/equinix/main.go
+++ b/cloud/shepherd/provider/equinix/main.go
@@ -14,6 +14,7 @@
 	"source.monogon.dev/cloud/equinix/wrapngo"
 	"source.monogon.dev/cloud/lib/component"
 	"source.monogon.dev/cloud/shepherd/manager"
+	ssh2 "source.monogon.dev/go/net/ssh"
 	clicontext "source.monogon.dev/metropolis/cli/pkg/context"
 )
 
@@ -89,7 +90,7 @@
 		klog.Exitf("%v", err)
 	}
 
-	sshClient := &manager.PlainSSHClient{
+	sshClient := &ssh2.DirectClient{
 		AuthMethod: ssh.PublicKeys(sshSigner),
 		// Equinix OS installations always use root.
 		Username: "root",
diff --git a/go/net/ssh/BUILD.bazel b/go/net/ssh/BUILD.bazel
new file mode 100644
index 0000000..cb82262
--- /dev/null
+++ b/go/net/ssh/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "ssh",
+    srcs = ["ssh_client.go"],
+    importpath = "source.monogon.dev/go/net/ssh",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_pkg_sftp//:sftp",
+        "@org_golang_x_crypto//ssh",
+    ],
+)
diff --git a/cloud/shepherd/manager/ssh_client.go b/go/net/ssh/ssh_client.go
similarity index 73%
rename from cloud/shepherd/manager/ssh_client.go
rename to go/net/ssh/ssh_client.go
index a1a305a..4e3cab6 100644
--- a/cloud/shepherd/manager/ssh_client.go
+++ b/go/net/ssh/ssh_client.go
@@ -1,4 +1,4 @@
-package manager
+package ssh
 
 import (
 	"bytes"
@@ -12,39 +12,39 @@
 	"golang.org/x/crypto/ssh"
 )
 
-// SSHClient defines a simple interface to an abstract SSH client. Usually this
-// would be PlainSSHClient, but tests can use this interface to dependency-inject
+// Client defines a simple interface to an abstract SSH client. Usually this
+// would be DirectClient, but tests can use this interface to dependency-inject
 // fake SSH connections.
-type SSHClient interface {
-	// Dial returns an SSHConnection to a given address (host:port pair) with
+type Client interface {
+	// Dial returns an Connection to a given address (host:port pair) with
 	// a timeout for connection.
-	Dial(ctx context.Context, address string, connectTimeout time.Duration) (SSHConnection, error)
+	Dial(ctx context.Context, address string, connectTimeout time.Duration) (Connection, error)
 }
 
-type SSHConnection interface {
+type Connection interface {
 	// Execute a given command on a remote host synchronously, passing in stdin as
 	// input, and returning a captured stdout/stderr. The returned data might be
 	// valid even when err != nil, which might happen if the remote side returned a
 	// non-zero exit code.
 	Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error)
 	// Upload a given blob to a targetPath on the system and make executable.
-	Upload(ctx context.Context, targetPath string, data []byte) error
+	Upload(ctx context.Context, targetPath string, src io.Reader) error
 	// Close this connection.
 	Close() error
 }
 
-// PlainSSHClient implements SSHClient (and SSHConnection) using
+// DirectClient implements Client (and Connection) using
 // golang.org/x/crypto/ssh.
-type PlainSSHClient struct {
+type DirectClient struct {
 	AuthMethod ssh.AuthMethod
 	Username   string
 }
 
-type plainSSHConn struct {
+type directConn struct {
 	cl *ssh.Client
 }
 
-func (p *PlainSSHClient) Dial(ctx context.Context, address string, connectTimeout time.Duration) (SSHConnection, error) {
+func (p *DirectClient) Dial(ctx context.Context, address string, connectTimeout time.Duration) (Connection, error) {
 	d := net.Dialer{
 		Timeout: connectTimeout,
 	}
@@ -69,12 +69,12 @@
 		return nil, err
 	}
 	cl := ssh.NewClient(conn2, chanC, reqC)
-	return &plainSSHConn{
+	return &directConn{
 		cl: cl,
 	}, nil
 }
 
-func (p *plainSSHConn) Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error) {
+func (p *directConn) Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error) {
 	sess, err := p.cl.NewSession()
 	if err != nil {
 		return nil, nil, fmt.Errorf("while creating SSH session: %w", err)
@@ -101,14 +101,13 @@
 	}
 }
 
-func (p *plainSSHConn) Upload(ctx context.Context, targetPath string, data []byte) error {
+func (p *directConn) Upload(ctx context.Context, targetPath string, src io.Reader) error {
 	sc, err := sftp.NewClient(p.cl)
 	if err != nil {
 		return fmt.Errorf("while building sftp client: %w", err)
 	}
 	defer sc.Close()
 
-	acrdr := bytes.NewReader(data)
 	df, err := sc.Create(targetPath)
 	if err != nil {
 		return fmt.Errorf("while creating file on the host: %w", err)
@@ -117,7 +116,7 @@
 	doneC := make(chan error, 1)
 
 	go func() {
-		_, err := io.Copy(df, acrdr)
+		_, err := io.Copy(df, src)
 		df.Close()
 		doneC <- err
 	}()
@@ -138,6 +137,6 @@
 	return nil
 }
 
-func (p *plainSSHConn) Close() error {
+func (p *directConn) Close() error {
 	return p.cl.Close()
 }