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()
}