Skip to content

Commit

Permalink
OSD-24621 - Add 'cluster ssh key' subcommand to find and print the ss…
Browse files Browse the repository at this point in the history
…h key for a given cluster
  • Loading branch information
tnierman committed Jul 18, 2024
1 parent cdbca4c commit 632e6a9
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/cluster/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/openshift/osdctl/cmd/cluster/access"
"github.com/openshift/osdctl/cmd/cluster/dynatrace"
"github.com/openshift/osdctl/cmd/cluster/resize"
"github.com/openshift/osdctl/cmd/cluster/ssh"
"github.com/openshift/osdctl/cmd/cluster/support"
"github.com/openshift/osdctl/internal/utils/globalflags"
"github.com/openshift/osdctl/pkg/k8s"
Expand Down Expand Up @@ -42,6 +43,7 @@ func NewCmdCluster(streams genericclioptions.IOStreams, client *k8s.LazyClient,
clusterCmd.AddCommand(dynatrace.NewCmdDynatrace())
clusterCmd.AddCommand(newCmdCleanupLeakedEC2())
clusterCmd.AddCommand(newCmdDetachStuckVolume())
clusterCmd.AddCommand(ssh.NewCmdSSH())
return clusterCmd
}

Expand Down
157 changes: 157 additions & 0 deletions cmd/cluster/ssh/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package ssh

import (
"context"
"fmt"
"strings"

"github.com/openshift/osdctl/pkg/k8s"
"github.com/openshift/osdctl/pkg/utils"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
// sshSecretName defines the name of the ssh secret in each hive namespace
sshSecretName = "ssh"
// privateKeyFilename defines the map key used to identify the private ssh key in the hive "ssh" secret's data
privateKeyFilename = "ssh-privatekey"
)

func NewCmdKey() *cobra.Command {
cmd := &cobra.Command{
Use: "key [cluster identifier]",
Short: "Retrieve a cluster's SSH key from Hive",
Long: "Retrieve a cluster's SSH key from Hive. If a cluster identifier (internal ID, UUID, name, etc) is provided, then the key retrieved will be for that cluster. If no identifier is provided, then the key for the cluster backplane is currently logged into will be used instead.",
Example: `$ osdctl cluster ssh key $CLUSTER_ID
INFO[0005] Backplane URL retrieved via OCM environment: https://api.backplane.openshift.com
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
Providing a $CLUSTER_ID allows you to specify the cluster who's private ssh key you want to view, regardless if you're logged in or not.
$ osdctl cluster ssh key
INFO[0005] Backplane URL retrieved via OCM environment: https://api.backplane.openshift.com
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
Omitting the $CLUSTER_ID will print the ssh key for the cluster you're currently logged into.
$ osdctl cluster ssh key > /tmp/ssh.key
INFO[0005] Backplane URL retrieved via OCM environment: https://api.backplane.openshift.com
$ cat /tmp/ssh.key
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
Despite the logs from backplane, the ssh key is the only output channelled through stdout. This means you can safely redirect the output to a file for greater convienence.`,
Args: cobra.MaximumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {

// If user provides an argument: use it to identify the cluster's hive shard,
// otherwise use the current cluster's ID
clusterID := ""
var err error
if len(args) == 0 {
clusterID, err = k8s.GetCurrentCluster()
if err != nil {
return fmt.Errorf("failed to retrieve ID for current cluster")
}
} else {
clusterID = args[0]
}

err = PrintKey(clusterID)
if err != nil {
return fmt.Errorf("failed to retrieve ssh key for cluster %s: %w", clusterID, err)
}
return nil
},
}
return cmd
}

// PrintKey retrieves the cluster's private ssh key from hive and prints it to stdout.
func PrintKey(identifier string) error {
// Login to the provided cluster's hive shard
ocmClient, err := utils.CreateConnection()
if err != nil {
return fmt.Errorf("failed to establish connection to OCM: %w", err)
}

cluster, err := utils.GetCluster(ocmClient, identifier)
if err != nil {
return fmt.Errorf("failed to retrieve cluster from OCM: %w", err)
}

clusterID := cluster.ID()
hive, err := utils.GetHiveCluster(clusterID)
if err != nil {
return fmt.Errorf("failed to retrieve hive shard for cluster: %w", err)
}

scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
hiveClient, err := k8s.NewAsBackplaneClusterAdmin(hive.ID(), client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create privileged client: %w", err)
}

// Determine the cluster's hive namespace via cluster ID
namespaces := corev1.NamespaceList{}
err = hiveClient.List(context.TODO(), &namespaces)
if err != nil {
return fmt.Errorf("failed to list hive namespaces: %w", err)
}

namespace, err := findClusterNamespace(namespaces, clusterID)
if err != nil {
return fmt.Errorf("failed to locate cluster namespace in hive: %w", err)
}

// Grab secret from the cluster's hive NS
secrets := corev1.SecretList{}
err = hiveClient.List(context.TODO(), &secrets, &client.ListOptions{Namespace: namespace.Name})
if err != nil {
return fmt.Errorf("failed to retrieve secrets from hive: %w", err)
}

secret, err := findSSHSecret(secrets)
if err != nil {
return fmt.Errorf("failed to retrieve ssh key from hive: %w", err)
}

// Grab the correct file out of the secret & decode
encodedPrivateKey, found := secret.Data[privateKeyFilename]
if !found {
return fmt.Errorf("failed to locate the private ssh key in the '%s/%s' secret from hive shard '%s'", secret.Namespace, secret.Name, hive.Name())
}

fmt.Println(string(encodedPrivateKey))

return nil
}

func findClusterNamespace(namespaces corev1.NamespaceList, clusterID string) (corev1.Namespace, error) {
for _, namespace := range namespaces.Items {
if strings.Contains(namespace.Name, clusterID) {
return namespace, nil
}
}
return corev1.Namespace{}, fmt.Errorf("no namespace containing the identifier '%s' found", clusterID)
}

func findSSHSecret(secrets corev1.SecretList) (corev1.Secret, error) {
for _, secret := range secrets.Items {
if secret.Name == sshSecretName {
return secret, nil
}
}
return corev1.Secret{}, fmt.Errorf("no secret named 'ssh' found")
}
147 changes: 147 additions & 0 deletions cmd/cluster/ssh/key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package ssh

import (
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
// Test namespaces
namespace1 = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "namespace1-abc",
},
}

namespace2 = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "namespace2-123",
},
}

namespace3 = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "namespace3-xyz",
},
}
)

func Test_findClusterNamespace(t *testing.T) {
type args struct {
namespaces corev1.NamespaceList
clusterID string
}
tests := []struct {
name string
args args
expected corev1.Namespace
expectErr bool
}{
{
name: "Single valid namespace",
args: args{
clusterID: "abc",
namespaces: corev1.NamespaceList{
Items: []corev1.Namespace{namespace1, namespace2, namespace3},
},
},
expected: namespace1,
expectErr: false,
},
{
name: "No valid namespaces",
args: args{
clusterID: "invalidclusterid",
namespaces: corev1.NamespaceList{
Items: []corev1.Namespace{namespace1, namespace2, namespace3},
},
},
expected: corev1.Namespace{},
expectErr: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := findClusterNamespace(test.args.namespaces, test.args.clusterID)
// Check whether the error status is the one we expect
if (err != nil) != test.expectErr {
t.Errorf("mismatch between resulting error and expected error:\ngot:\n%v\n\nexpected:\n%v", err, test.expectErr)
return
}

// Check the actual results of the test
if result.Name != test.expected.Name {
t.Errorf("mismatch between test result and expected value:\ngot:\n%#v\n\nexpected:\n%#v", result, test.expected)
}
})
}
}

var (
secret1 = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret1",
},
}

secret2 = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret2",
},
}

sshSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ssh",
},
}
)

func Test_findSSHSecret(t *testing.T) {
type args struct {
secrets corev1.SecretList
}
tests := []struct {
name string
args args
expected corev1.Secret
expectErr bool
}{
{
name: "Single valid secret",
args: args{
secrets: corev1.SecretList{ Items: []corev1.Secret{secret1, secret2, sshSecret} },
},
expected: sshSecret,
expectErr: false,
},
{
name: "No valid secrets",
args: args{
secrets: corev1.SecretList{ Items: []corev1.Secret{secret1, secret2} },
},
expected: corev1.Secret{},
expectErr: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := findSSHSecret(test.args.secrets)
// Check whether the error status is the one we expect
if (err != nil) != test.expectErr {
t.Errorf("mismatch between resulting error and expected error:\ngot:\n%v\n\nexpected:\n%v", err, test.expectErr)
return
}

// Check the actual results of the test
if result.Name != test.expected.Name {
t.Errorf("mismatch between test result and expected value:\ngot:\n%#v\n\nexpected:\n%#v", result, test.expected)
}

})
}
}
13 changes: 13 additions & 0 deletions cmd/cluster/ssh/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ssh

import "github.com/spf13/cobra"

func NewCmdSSH() *cobra.Command {
cmd := &cobra.Command{
Use: "ssh",
Short: "utilities for accessing cluster via ssh",
}

cmd.AddCommand(NewCmdKey())
return cmd
}
9 changes: 9 additions & 0 deletions pkg/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

bplogin "github.com/openshift/backplane-cli/cmd/ocm-backplane/login"
bputils "github.com/openshift/backplane-cli/pkg/utils"
bpconfig "github.com/openshift/backplane-cli/pkg/cli/config"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -161,3 +162,11 @@ func NewAsBackplaneClusterAdmin(clusterID string, options client.Options, elevat

return client.New(cfg, options)
}

func GetCurrentCluster() (string, error) {
cluster, err := bputils.DefaultClusterUtils.GetBackplaneClusterFromConfig()
if err != nil {
return "", fmt.Errorf("failed to retrieve backplane status: %v", err)
}
return cluster.ClusterID, nil
}

0 comments on commit 632e6a9

Please sign in to comment.