From c0acb9ddea955fdbd4403565b76b11183a7dc2c3 Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:17:52 +0100 Subject: [PATCH 1/3] test(e2e): add ssh key e2e tests --- test/e2e/sshkey_test.go | 166 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 test/e2e/sshkey_test.go diff --git a/test/e2e/sshkey_test.go b/test/e2e/sshkey_test.go new file mode 100644 index 00000000..308b5e26 --- /dev/null +++ b/test/e2e/sshkey_test.go @@ -0,0 +1,166 @@ +//go:build e2e + +package e2e + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/swaggest/assertjson" + "golang.org/x/crypto/ssh" + + "github.com/hetznercloud/hcloud-go/v2/hcloud" +) + +func TestSSHKey(t *testing.T) { + t.Parallel() + + pubKey, fingerprint, err := generateSSHKey() + require.NoError(t, err) + + sshKeyName := withSuffix("test-ssh-key") + sshKeyID, err := createSSHKey(t, sshKeyName, "--public-key", pubKey) + require.NoError(t, err) + + t.Run("add-label", func(t *testing.T) { + t.Run("non-existing", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "add-label", "non-existing-ssh-key", "foo=bar") + require.EqualError(t, err, "ssh key not found: non-existing-ssh-key") + assert.Empty(t, out) + }) + + t.Run("1", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "add-label", strconv.FormatInt(sshKeyID, 10), "foo=bar") + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("Label(s) foo added to SSH Key %d\n", sshKeyID), out) + }) + + t.Run("2", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "add-label", strconv.FormatInt(sshKeyID, 10), "baz=qux") + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("Label(s) baz added to SSH Key %d\n", sshKeyID), out) + }) + }) + + t.Run("list", func(t *testing.T) { + t.Run("table", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "list", "-o=columns=id,name,fingerprint,public_key,labels,created,age") + require.NoError(t, err) + assert.Regexp(t, + NewRegex().Start(). + SeparatedByWhitespace("ID", "NAME", "FINGERPRINT", "PUBLIC KEY", "LABELS", "CREATED", "AGE").Newline(). + Lit(strconv.FormatInt(sshKeyID, 10)).Whitespace(). + Lit(sshKeyName).Whitespace(). + Lit(fingerprint).Whitespace(). + Lit(pubKey).Whitespace(). + Lit("baz=qux, foo=bar").Whitespace(). + UnixDate().Whitespace(). + Age().Newline(). + End(), + out, + ) + }) + + t.Run("json", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "list", "-o=json") + require.NoError(t, err) + assertjson.Equal(t, []byte(fmt.Sprintf(` +[ + { + "id": %d, + "name": %q, + "fingerprint": %q, + "public_key": %q, + "labels": { + "baz": "qux", + "foo": "bar" + }, + "created": "" + } +]`, sshKeyID, sshKeyName, fingerprint, pubKey)), []byte(out)) + }) + }) + + t.Run("remove-label", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "remove-label", strconv.FormatInt(sshKeyID, 10), "baz") + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("Label(s) baz removed from SSH Key %d\n", sshKeyID), out) + }) + + t.Run("update-name", func(t *testing.T) { + sshKeyName = withSuffix("new-test-ssh-key") + out, err := runCommand(t, "ssh-key", "update", strconv.FormatInt(sshKeyID, 10), "--name", sshKeyName) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("SSHKey %d updated\n", sshKeyID), out) + }) + + t.Run("describe", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "describe", strconv.FormatInt(sshKeyID, 10)) + require.NoError(t, err) + assert.Regexp(t, NewRegex().Start(). + Lit("ID:").Whitespace().Lit(strconv.FormatInt(sshKeyID, 10)).Newline(). + Lit("Name:").Whitespace().Lit(sshKeyName).Newline(). + Lit("Created:").Whitespace().UnixDate().Lit(" (").HumanizeTime().Lit(")").Newline(). + Lit("Fingerprint:").Whitespace().Lit(fingerprint).Newline(). + Lit("Public Key:").Newline().Lit(pubKey). + Lit("Labels:").Newline(). + Lit(" foo:").Whitespace().Lit("bar").Newline(). + End(), + out, + ) + }) + + t.Run("delete", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "delete", strconv.FormatInt(sshKeyID, 10)) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("SSH Key %d deleted\n", sshKeyID), out) + }) +} + +func createSSHKey(t *testing.T, name string, args ...string) (int64, error) { + t.Helper() + t.Cleanup(func() { + _, _ = client.SSHKey.Delete(context.Background(), &hcloud.SSHKey{Name: name}) + }) + + out, err := runCommand(t, append([]string{"ssh-key", "create", "--name", name}, args...)...) + if err != nil { + return 0, err + } + + if !assert.Regexp(t, `^SSH key [0-9]+ created\n$`, out) { + return 0, fmt.Errorf("invalid response: %s", out) + } + + id, err := strconv.ParseInt(out[8:len(out)-9], 10, 64) + if err != nil { + return 0, err + } + + t.Cleanup(func() { + _, _ = client.SSHKey.Delete(context.Background(), &hcloud.SSHKey{ID: id}) + }) + return id, nil +} + +func generateSSHKey() (string, string, error) { + pub, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", "", err + } + + pubKey, err := ssh.NewPublicKey(pub) + if err != nil { + return "", "", err + } + + fingerprint := ssh.FingerprintLegacyMD5(pubKey) + pubKeyBytes := ssh.MarshalAuthorizedKey(pubKey) + return string(pubKeyBytes), fingerprint, nil +} From 9536fdf89bdd26369552b933152f68d45756b40b Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Fri, 3 Jan 2025 10:07:20 +0100 Subject: [PATCH 2/3] use sshutil package --- test/e2e/sshkey_test.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/e2e/sshkey_test.go b/test/e2e/sshkey_test.go index 308b5e26..1fae1fbe 100644 --- a/test/e2e/sshkey_test.go +++ b/test/e2e/sshkey_test.go @@ -4,8 +4,6 @@ package e2e import ( "context" - "crypto/ed25519" - "crypto/rand" "fmt" "strconv" "testing" @@ -13,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/swaggest/assertjson" - "golang.org/x/crypto/ssh" "github.com/hetznercloud/hcloud-go/v2/hcloud" + "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/kit/sshutil" ) func TestSSHKey(t *testing.T) { @@ -150,17 +148,17 @@ func createSSHKey(t *testing.T, name string, args ...string) (int64, error) { } func generateSSHKey() (string, string, error) { - pub, _, err := ed25519.GenerateKey(rand.Reader) + // ed25519 SSH key + _, pub, err := sshutil.GenerateKeyPair() if err != nil { return "", "", err } - pubKey, err := ssh.NewPublicKey(pub) + // MD5 fingerprint + fingerprint, err := sshutil.GetPublicKeyFingerprint(pub) if err != nil { return "", "", err } - fingerprint := ssh.FingerprintLegacyMD5(pubKey) - pubKeyBytes := ssh.MarshalAuthorizedKey(pubKey) - return string(pubKeyBytes), fingerprint, nil + return string(pub), fingerprint, nil } From 8ba3eb340a4315ace8626b90191695ac1fba6a98 Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:34:29 +0100 Subject: [PATCH 3/3] add ssh key to combined test --- test/e2e/combined_test.go | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/test/e2e/combined_test.go b/test/e2e/combined_test.go index 6370f7b1..d576fef9 100644 --- a/test/e2e/combined_test.go +++ b/test/e2e/combined_test.go @@ -4,17 +4,35 @@ package e2e import ( "fmt" + "os" + "path" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/kit/sshutil" ) func TestCombined(t *testing.T) { // combined tests combine multiple resources and can thus not be run in parallel + priv, pub, err := sshutil.GenerateKeyPair() + require.NoError(t, err) + + keyDir := t.TempDir() + pubKeyPath, privKeyPath := path.Join(keyDir, "id_ed25519.pub"), path.Join(keyDir, "id_ed25519") + err = os.WriteFile(privKeyPath, priv, 0600) + require.NoError(t, err) + err = os.WriteFile(pubKeyPath, pub, 0644) + require.NoError(t, err) + + sshKeyName := withSuffix("test-ssh-key") + sshKeyID, err := createSSHKey(t, sshKeyName, "--public-key-from-file", pubKeyPath) + require.NoError(t, err) + serverName := withSuffix("test-server") - serverID, err := createServer(t, serverName, TestServerType, TestImage) + serverID, err := createServer(t, serverName, TestServerType, TestImage, "--ssh-key", strconv.FormatInt(sshKeyID, 10)) require.NoError(t, err) firewallName := withSuffix("test-firewall") @@ -105,9 +123,28 @@ func TestCombined(t *testing.T) { }) }) + t.Run("ssh", func(t *testing.T) { + out, err := runCommand( + t, "server", "ssh", strconv.FormatInt(serverID, 10), + "-i", privKeyPath, + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "IdentitiesOnly=yes", + "--", "exit", + ) + require.NoError(t, err) + assert.Empty(t, out) + }) + t.Run("delete-server", func(t *testing.T) { out, err := runCommand(t, "server", "delete", strconv.FormatInt(serverID, 10)) require.NoError(t, err) assert.Equal(t, fmt.Sprintf("Server %d deleted\n", serverID), out) }) + + t.Run("delete-ssh-key", func(t *testing.T) { + out, err := runCommand(t, "ssh-key", "delete", strconv.FormatInt(sshKeyID, 10)) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("SSH Key %d deleted\n", sshKeyID), out) + }) }