@@ -6,6 +6,7 @@ package node
66import (
77 "context"
88 "fmt"
9+ "strings"
910
1011 "github.com/sirupsen/logrus"
1112 "github.com/spf13/cobra"
@@ -18,17 +19,42 @@ import (
1819
1920func newSSHCmd () * cobra.Command {
2021 cmd := & cobra.Command {
21- Use : "ssh <node-name>" ,
22+ Use : "ssh <node-name> [-- command [args...]] " ,
2223 Short : "SSH into a node's VM" ,
23- Long : "Start an interactive SSH session to a node's VM" ,
24+ Long : "Start an interactive SSH session to a node's VM, or run a command and exit. " ,
2425 Example : ` # SSH into the control-plane node
2526 bink node ssh node1
2627
2728 # SSH into a worker node in a named cluster
28- bink node ssh node2 --cluster-name dev` ,
29- Args : cobra .ExactArgs (1 ),
30- ValidArgsFunction : cli .CompleteNodeNames ,
31- RunE : runSSH ,
29+ bink node ssh node2 --cluster-name dev
30+
31+ # Run a command on a node
32+ bink node ssh node1 -- uname -a
33+
34+ # Run kubectl on the control-plane node
35+ bink node ssh node1 -- sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get nodes` ,
36+ Args : func (cmd * cobra.Command , args []string ) error {
37+ dash := cmd .ArgsLenAtDash ()
38+ switch {
39+ case dash == - 1 && len (args ) == 1 :
40+ return nil
41+ case dash == 1 :
42+ return nil
43+ case dash == - 1 && len (args ) == 0 :
44+ return fmt .Errorf ("requires a node name" )
45+ case dash == - 1 :
46+ return fmt .Errorf ("extra arguments %v; use -- to separate the remote command" , args [1 :])
47+ default :
48+ return fmt .Errorf ("exactly one node name is required before --" )
49+ }
50+ },
51+ ValidArgsFunction : func (cmd * cobra.Command , args []string , toComplete string ) ([]string , cobra.ShellCompDirective ) {
52+ if len (args ) > 0 {
53+ return nil , cobra .ShellCompDirectiveNoFileComp
54+ }
55+ return cli .CompleteNodeNames (cmd , args , toComplete )
56+ },
57+ RunE : runSSH ,
3258 }
3359
3460 return cmd
@@ -40,19 +66,25 @@ func runSSH(cmd *cobra.Command, args []string) error {
4066 ctx := context .Background ()
4167 logger := logrus .New ()
4268
43- // Get cluster name
4469 clusterName := viper .GetString ("cluster.name" )
45-
46- // Get node IP for display
47- clusterIP := node .CalculateClusterIP (clusterName , nodeName )
48-
49- // Create SSH client
5070 sshClient := ssh .NewClientForNode (clusterName , nodeName , logger )
5171
72+ if cmd .ArgsLenAtDash () == 1 {
73+ command := strings .Join (args [1 :], " " )
74+ exitCode , err := sshClient .ExecStream (ctx , command )
75+ if err != nil {
76+ return fmt .Errorf ("failed to execute command on %s: %w" , nodeName , err )
77+ }
78+ if exitCode != 0 {
79+ return & cli.ExitCodeError {Code : exitCode }
80+ }
81+ return nil
82+ }
83+
84+ clusterIP := node .CalculateClusterIP (clusterName , nodeName )
5285 fmt .Printf ("Connecting to %s (SSH: %s:%s, cluster: %s) as user core\n " ,
5386 nodeName , ssh .DefaultSSHHost , ssh .DefaultSSHPort , clusterIP )
5487
55- // Start interactive session
5688 if err := sshClient .Interactive (ctx ); err != nil {
5789 return fmt .Errorf ("failed to connect to %s: %w" , nodeName , err )
5890 }
0 commit comments