-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OSD-24621 - Add 'cluster ssh key' subcommand to find and print the ss…
…h key for a given cluster
- Loading branch information
Showing
5 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 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,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") | ||
} |
This file contains 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,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) | ||
} | ||
|
||
}) | ||
} | ||
} |
This file contains 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,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 | ||
} |
This file contains 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