Skip to content

Commit d1c3ba5

Browse files
authored
Add changes (#585)
1 parent f850442 commit d1c3ba5

File tree

8 files changed

+559
-1
lines changed

8 files changed

+559
-1
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ mock-gen:
120120
mockgen -destination=./pkg/healthcheck/mocks/httpClientMock.go -package=mocks github.com/openshift/backplane-cli/pkg/healthcheck HTTPClient
121121
mockgen -destination=./pkg/info/mocks/infoMock.go -package=mocks github.com/openshift/backplane-cli/pkg/info InfoService
122122
mockgen -destination=./pkg/info/mocks/buildInfoMock.go -package=mocks github.com/openshift/backplane-cli/pkg/info BuildInfoService
123+
mockgen -destination=./pkg/ssm/mocks/mock_ssmclient.go -package=mocks github.com/openshift/backplane-cli/pkg/ssm SSMClient
123124

124125
.PHONY: build-image
125126
build-image:

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ To setup the PS1(prompt) for bash/zsh, please follow [these instructions](https:
6464
| `ocm backplane console [flags]` | Launch the OpenShift console of the current logged in cluster |
6565
| `ocm backplane cloud console` | Launch the current logged in cluster's cloud provider console |
6666
| `ocm backplane cloud credentials [flags]` | Retrieve a set of temporary cloud credentials for the cluster's cloud provider |
67+
| `ocm backplane cloud ssm --node <node-name>` | Start an aws ssm session for an HCP cluster |
6768
| `ocm backplane elevate <reason> -- <command>` | Elevate privileges to backplane-cluster-admin and add a reason to the api request, this reason will be stored for 20min for future usage |
6869
| `ocm backplane monitoring <prometheus/alertmanager/thanos/grafana> [flags]` | Launch the specified monitoring UI (Deprecated following v4.11 for cluster monitoring stack)|
6970
| `ocm backplane script describe <script> [flags]` | Describe the given backplane script |
@@ -227,7 +228,27 @@ Logging into multiple clusters via different terminal instances.
227228
```
228229
$ export BACKPLANE_DEFAULT_OPEN_BROWSER=true
229230
$ ocm backplane cloud console
230-
`
231+
```
232+
233+
## SSM Session
234+
Now you can directly start an AWS SSM session in your terminal using a single command for the HCP clusters without logging into their cloud consoles. It will start an AWS session directly in your terminal where you can debug into the worker node for the HCP cluster and carry out further operations.
235+
- Before using ssm command check if Session Manager plugin has been properly set up in your device. Follow this official AWS [documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-prerequisites.html) for further information on setting up AWS SSM. And for installing SSM plugin directly on your device follow this [documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html). Also check AWS CLI version and SSM version and ensure that those are in required versions. Update if required.
236+
- Login to the management cluster for the target HCP cluster
237+
```
238+
$ ocm backplane login <cluster> --manager
239+
```
240+
- Run the following command by providing the correct worker node name where you want to start a SSM Session
241+
```
242+
$ ocm backplane cloud ssm --node <node-name>`
243+
```
244+
- It will show something like this:
245+
```
246+
$ ocm-backplane cloud ssm --node ip-xx-x-xxx-xxx.xxxxxx.compute.internal
247+
248+
Starting session with SessionId: e4abf0bf76199710d76b3ecaa2c6f4ae-929p9nkk8yta7upy8el4ylidq4
249+
sh-5.1$
250+
```
251+
231252
## Monitoring
232253
Monitoring command can be used to launch the specified monitoring UI.
233254

cmd/ocm-backplane/cloud/cloud.go

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var CloudCmd = &cobra.Command{
1818
func init() {
1919
CloudCmd.AddCommand(CredentialsCmd)
2020
CloudCmd.AddCommand(ConsoleCmd)
21+
CloudCmd.AddCommand(SSMSessionCmd)
2122
}
2223

2324
func help(cmd *cobra.Command, _ []string) {

cmd/ocm-backplane/cloud/ssm.go

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package cloud
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
11+
"github.com/aws/aws-sdk-go-v2/aws"
12+
awsConfig "github.com/aws/aws-sdk-go-v2/config"
13+
"github.com/aws/aws-sdk-go-v2/service/ssm"
14+
"github.com/openshift/backplane-cli/pkg/cli/config"
15+
bpCredentials "github.com/openshift/backplane-cli/pkg/credentials"
16+
"github.com/openshift/backplane-cli/pkg/ocm"
17+
logger "github.com/sirupsen/logrus"
18+
"github.com/spf13/cobra"
19+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"k8s.io/cli-runtime/pkg/genericclioptions"
21+
"k8s.io/client-go/kubernetes"
22+
"k8s.io/client-go/rest"
23+
)
24+
25+
var (
26+
CreateClientSet func(*rest.Config) (kubernetes.Interface, error) = createClientSet
27+
GetInstanceID func(node string, config *rest.Config) (string, error) = getInstanceID
28+
StartSSMsession func(cmd *cobra.Command, argv []string) error = startSSMsession
29+
ExecCommand func(name string, arg ...string) *exec.Cmd = exec.Command
30+
NewFromConfig func(cfg aws.Config) SSMClient = NewFromConfigVar
31+
GetCurrentKubeconfig func() (*rest.Config, error) = getCurrentKubeconfig
32+
FetchCloudCredentials func() (*bpCredentials.AWSCredentialsResponse, error) = fetchCloudCredentials
33+
)
34+
35+
func NewFromConfigVar(cfg aws.Config) SSMClient {
36+
return ssm.NewFromConfig(cfg)
37+
}
38+
39+
var (
40+
createClientSet = func(c *rest.Config) (kubernetes.Interface, error) { return kubernetes.NewForConfig(c) }
41+
)
42+
43+
var ssmArgs struct {
44+
node string
45+
}
46+
47+
var SSMSessionCmd = &cobra.Command{
48+
Use: "ssm",
49+
Short: "Start an AWS SSM session for a node",
50+
Long: "Start an AWS SSM session for the specified node provided to debug.",
51+
Args: cobra.ExactArgs(0),
52+
RunE: startSSMsession,
53+
}
54+
55+
func init() {
56+
SSMSessionCmd.Flags().StringVar(&ssmArgs.node, "node", "", "Specify the node name to start the SSM session.")
57+
}
58+
59+
func fetchCloudCredentials() (*bpCredentials.AWSCredentialsResponse, error) {
60+
var clusterKey string
61+
clusterInfo, err := GetBackplaneClusterFromConfig()
62+
if err != nil {
63+
return nil, fmt.Errorf("expected exactly one cluster: %w", err)
64+
}
65+
clusterKey = clusterInfo.ClusterID
66+
67+
clusterID, clusterName, err := ocm.DefaultOCMInterface.GetTargetCluster(clusterKey)
68+
if err != nil {
69+
return nil, fmt.Errorf("expected exactly one cluster: %w", err)
70+
}
71+
72+
cluster, err := ocm.DefaultOCMInterface.GetClusterInfoByID(clusterID)
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to get cluster info for %s: %w", clusterID, err)
75+
}
76+
77+
logger.WithFields(logger.Fields{
78+
"ID": clusterID,
79+
"Name": clusterName}).Infoln("Target cluster")
80+
81+
backplaneConfig, err := config.GetBackplaneConfiguration()
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to get backplane configuration: %w", err)
84+
}
85+
86+
ocmConnection, err := ocm.DefaultOCMInterface.SetupOCMConnection()
87+
if err != nil {
88+
return nil, fmt.Errorf("failed to create OCM connection: %w", err)
89+
}
90+
defer ocmConnection.Close()
91+
92+
queryConfig := &QueryConfig{OcmConnection: ocmConnection, BackplaneConfiguration: backplaneConfig, Cluster: cluster}
93+
94+
creds, err := queryConfig.GetCloudCredentials()
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to fetch cloud credentials: %w", err)
97+
}
98+
99+
awsCreds, ok := creds.(*bpCredentials.AWSCredentialsResponse)
100+
if !ok {
101+
return nil, fmt.Errorf("unexpected credentials type: %T", creds)
102+
}
103+
104+
logger.Info("Successfully fetched cloud credentials.")
105+
return awsCreds, nil
106+
}
107+
108+
func getInstanceID(nodeName string, config *rest.Config) (string, error) {
109+
clientset, err := CreateClientSet(config)
110+
if err != nil {
111+
return "", fmt.Errorf("failed to create client: %w", err)
112+
}
113+
114+
node, err := clientset.CoreV1().Nodes().Get(context.TODO(), nodeName, metav1.GetOptions{})
115+
if err != nil {
116+
return "", fmt.Errorf("failed to get node %s: %w", nodeName, err)
117+
}
118+
119+
if node.Spec.ProviderID == "" {
120+
return "", fmt.Errorf("providerID is not set")
121+
}
122+
123+
// Extract instance ID from ProviderID
124+
instanceID := strings.Split(node.Spec.ProviderID, "/")[len(strings.Split(node.Spec.ProviderID, "/"))-1]
125+
return instanceID, nil
126+
}
127+
128+
func startSSMsession(cmd *cobra.Command, argv []string) error {
129+
if ssmArgs.node == "" {
130+
return fmt.Errorf("--node flag is required")
131+
}
132+
133+
// Fetch cloud credentials and export them as environment variables
134+
creds, err := FetchCloudCredentials() // Use the variable instead of direct call
135+
if err != nil {
136+
return fmt.Errorf("failed to fetch cloud credentials: %w", err)
137+
}
138+
139+
// Set AWS credentials in environment variables
140+
os.Setenv("AWS_ACCESS_KEY_ID", creds.AccessKeyID)
141+
os.Setenv("AWS_SECRET_ACCESS_KEY", creds.SecretAccessKey)
142+
os.Setenv("AWS_SESSION_TOKEN", creds.SessionToken)
143+
144+
kubeconfig, err := getCurrentKubeconfig()
145+
if err != nil {
146+
return fmt.Errorf("failed to get kubeconfig: %w", err)
147+
}
148+
149+
instanceID, err := getInstanceID(ssmArgs.node, kubeconfig)
150+
if err != nil {
151+
return fmt.Errorf("failed to get instance ID for node %s: %w", ssmArgs.node, err)
152+
}
153+
154+
logger.Infof("Starting SSM session for node: %s with Instance ID: %s", ssmArgs.node, instanceID)
155+
156+
cfg, err := awsConfig.LoadDefaultConfig(context.TODO(), awsConfig.WithRegion(creds.Region))
157+
if err != nil {
158+
return fmt.Errorf("unable to load SDK config: %v", err)
159+
}
160+
ssmClient := NewFromConfig(cfg)
161+
162+
input := &ssm.StartSessionInput{
163+
Target: aws.String(instanceID),
164+
}
165+
166+
result, err := ssmClient.StartSession(context.TODO(), input)
167+
if err != nil {
168+
return fmt.Errorf("failed to start SSM session via SDK: %w", err)
169+
}
170+
171+
// Ensure the session details are not nil
172+
if result.SessionId == nil || result.StreamUrl == nil || result.TokenValue == nil {
173+
return fmt.Errorf("session details are incomplete: SessionId=%v, StreamUrl=%v, TokenValue=%v", result.SessionId, result.StreamUrl, result.TokenValue)
174+
}
175+
176+
// Log session details for debugging
177+
logger.Infof("SessionId: %v", *result.SessionId)
178+
logger.Infof("StreamUrl: %v", *result.StreamUrl)
179+
logger.Infof("TokenValue: %v", *result.TokenValue)
180+
181+
// Prepare arguments for Session Manager Plugin
182+
sessionJSON, err := json.Marshal(map[string]string{
183+
"SessionId": *result.SessionId,
184+
"StreamUrl": *result.StreamUrl,
185+
"TokenValue": *result.TokenValue,
186+
})
187+
if err != nil {
188+
return fmt.Errorf("failed to serialize session details: %w", err)
189+
}
190+
191+
//Check if session command is installed
192+
ValidateSessionCmd := ExecCommand("session-manager-plugin", "--version")
193+
err = ValidateSessionCmd.Run()
194+
if err != nil {
195+
return fmt.Errorf("session-manager-plugin is not installed. Please refer AWS doc: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html")
196+
}
197+
198+
cmdArgs := []string{"session-manager-plugin", string(sessionJSON), creds.Region, "StartSession"}
199+
pluginCmd := ExecCommand(cmdArgs[0], cmdArgs[1:]...) //#nosec G204: Command arguments are trusted
200+
pluginCmd.Stdout = os.Stdout
201+
pluginCmd.Stderr = os.Stderr
202+
pluginCmd.Stdin = os.Stdin
203+
204+
return pluginCmd.Run()
205+
}
206+
207+
func getCurrentKubeconfig() (*rest.Config, error) {
208+
cf := genericclioptions.NewConfigFlags(true)
209+
config, err := cf.ToRESTConfig()
210+
if err != nil {
211+
return nil, err
212+
}
213+
return config, nil
214+
}
215+
216+
// Define SSMClient interface
217+
type SSMClient interface {
218+
StartSession(ctx context.Context, params *ssm.StartSessionInput, optFns ...func(*ssm.Options)) (*ssm.StartSessionOutput, error)
219+
}

0 commit comments

Comments
 (0)