From eb87afbbb9d079603e4f0f1c76e41f577eb19ebf Mon Sep 17 00:00:00 2001 From: Ted Teng Date: Wed, 30 Aug 2023 10:13:40 +0800 Subject: [PATCH 1/5] add sshuser flag --- docs/help/gardenctl_ssh.md | 1 + pkg/cmd/ssh/options.go | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/help/gardenctl_ssh.md b/docs/help/gardenctl_ssh.md index 29fdcdaf..2aa91ed0 100644 --- a/docs/help/gardenctl_ssh.md +++ b/docs/help/gardenctl_ssh.md @@ -58,6 +58,7 @@ gardenctl ssh --keep-bastion --bastion-name cli-xxxxxxxx --public-key-file /path --seed string target the given seed cluster --shoot string target the given shoot cluster --skip-availability-check Skip checking for SSH bastion host availability. + --ssh-user string ssh user is the name of the Shoot cluster node ssh login user name. --wait-timeout duration Maximum duration to wait for the bastion to become available. (default 10m0s) ``` diff --git a/pkg/cmd/ssh/options.go b/pkg/cmd/ssh/options.go index d7e6c473..182137cd 100644 --- a/pkg/cmd/ssh/options.go +++ b/pkg/cmd/ssh/options.go @@ -55,12 +55,15 @@ import ( const ( // SSHBastionUsername is the system username on the bastion host. SSHBastionUsername = "gardener" - // SSHNodeUsername is the system username on any of the shoot cluster nodes. - SSHNodeUsername = "gardener" + // DefaultUsername is the default system username on any of the shoot cluster nodes. + DefaultUsername = "gardener" // SSHPort is the TCP port on a bastion instance that allows incoming SSH. SSHPort = 22 ) +// SSHNodeUsername is ssh login user name. +var SSHNodeUsername string + // wrappers used for unit tests only. var ( // keepAliveInterval is the interval in which bastions should be given the @@ -194,6 +197,9 @@ type SSHOptions struct { // bastion host, but leave it up to the user to SSH themselves. NodeName string + // SSHUser is the name of the Shoot cluster node ssh login user name + SSHUser string + // SSHPublicKeyFile is the full path to the file containing the user's // public SSH key. If not given, gardenctl will create a new temporary keypair. SSHPublicKeyFile PublicKeyFile @@ -258,7 +264,7 @@ func (o *SSHOptions) AddFlags(flagSet *pflag.FlagSet) { flagSet.StringVar(&o.BastionPort, "bastion-port", o.BastionPort, "SSH port of the bastion used for the SSH client command. Defaults to port 22") flagSet.StringSliceVar(&o.BastionUserKnownHostsFiles, "bastion-user-known-hosts-file", o.BastionUserKnownHostsFiles, "Path to a custom known hosts file for the SSH connection to the bastion. This file is used to verify the public keys of remote hosts when establishing a secure connection.") flagSet.BoolVarP(&o.ConfirmAccessRestriction, "confirm-access-restriction", "y", o.ConfirmAccessRestriction, "Bypasses the need for confirmation of any access restrictions. Set this flag only if you are fully aware of the access restrictions.") - + flagSet.StringVar(&o.SSHUser, "ssh-user", o.SSHUser, "ssh user is the name of the Shoot cluster node ssh login user name.") o.Options.AddFlags(flagSet) } @@ -310,6 +316,12 @@ func (o *SSHOptions) Complete(f util.Factory, cmd *cobra.Command, args []string) o.BastionName = name } + if o.SSHUser == "" { + SSHNodeUsername = DefaultUsername + } else { + SSHNodeUsername = o.SSHUser + } + return nil } From 8cec543b69bb942c64669e8d530b1002bdceef6d Mon Sep 17 00:00:00 2001 From: Ted Teng Date: Thu, 7 Sep 2023 15:54:03 +0800 Subject: [PATCH 2/5] add options fields --- docs/help/gardenctl_ssh.md | 2 +- pkg/cmd/ssh/arguments.go | 3 ++- pkg/cmd/ssh/arguments_test.go | 15 +++++++++++++++ pkg/cmd/ssh/connect_information.go | 1 + pkg/cmd/ssh/export_test.go | 2 ++ pkg/cmd/ssh/options.go | 25 ++++++++++++------------- pkg/cmd/ssh/ssh_test.go | 4 ++-- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/docs/help/gardenctl_ssh.md b/docs/help/gardenctl_ssh.md index 2aa91ed0..942dff3f 100644 --- a/docs/help/gardenctl_ssh.md +++ b/docs/help/gardenctl_ssh.md @@ -58,7 +58,7 @@ gardenctl ssh --keep-bastion --bastion-name cli-xxxxxxxx --public-key-file /path --seed string target the given seed cluster --shoot string target the given shoot cluster --skip-availability-check Skip checking for SSH bastion host availability. - --ssh-user string ssh user is the name of the Shoot cluster node ssh login user name. + --user string user is the name of the Shoot cluster node ssh login user name. (default "gardener") --wait-timeout duration Maximum duration to wait for the bastion to become available. (default 10m0s) ``` diff --git a/pkg/cmd/ssh/arguments.go b/pkg/cmd/ssh/arguments.go index 00c5a0b5..a02be2dc 100644 --- a/pkg/cmd/ssh/arguments.go +++ b/pkg/cmd/ssh/arguments.go @@ -66,6 +66,7 @@ func sshCommandArguments( bastionUserKnownHostsFiles []string, nodeHostname string, nodePrivateKeyFiles []PrivateKeyFile, + user string, ) arguments { bastionUserKnownHostsFilesArg := userKnownHostsFilesArgument(bastionUserKnownHostsFiles) @@ -87,7 +88,7 @@ func sshCommandArguments( args = append(args, argument{value: fmt.Sprintf("-oProxyCommand=%s", proxyCmdArgs.String())}) - args = append(args, argument{value: fmt.Sprintf("%s@%s", SSHNodeUsername, nodeHostname)}) + args = append(args, argument{value: fmt.Sprintf("%s@%s", user, nodeHostname)}) return arguments{list: args} } diff --git a/pkg/cmd/ssh/arguments_test.go b/pkg/cmd/ssh/arguments_test.go index 74628135..024600ab 100644 --- a/pkg/cmd/ssh/arguments_test.go +++ b/pkg/cmd/ssh/arguments_test.go @@ -23,6 +23,7 @@ type testCase struct { nodeHostname string nodePrivateKeyFiles []ssh.PrivateKeyFile expectedArgs []string + user string } func newTestCase() testCase { @@ -32,6 +33,7 @@ func newTestCase() testCase { sshPrivateKeyFile: "path/to/private/key", nodeHostname: "node.example.com", nodePrivateKeyFiles: []ssh.PrivateKeyFile{"path/to/node/private/key"}, + user: "gardener", } } @@ -46,6 +48,7 @@ var _ = Describe("Arguments", func() { tc.bastionUserKnownHostsFiles, tc.nodeHostname, tc.nodePrivateKeyFiles, + tc.user, ) Expect(args.String()).To(Equal(strings.Join(tc.expectedArgs, " "))) }, @@ -60,6 +63,18 @@ var _ = Describe("Arguments", func() { } return tc }()), + Entry("basic case with other ssh username", func() testCase { + tc := newTestCase() + tc.user = "aaa" + tc.expectedArgs = []string{ + "-oStrictHostKeyChecking=no", + "-oIdentitiesOnly=yes", + "'-ipath/to/node/private/key'", + `'-oProxyCommand=ssh -W%h:%p -oStrictHostKeyChecking=no -oIdentitiesOnly=yes '"'"'-ipath/to/private/key'"'"' '"'"'gardener@bastion.example.com'"'"' '"'"'-p22'"'"''`, + "'aaa@node.example.com'", + } + return tc + }()), Entry("no bastion port", func() testCase { tc := newTestCase() tc.bastionPort = "" diff --git a/pkg/cmd/ssh/connect_information.go b/pkg/cmd/ssh/connect_information.go index de21960b..aa437be1 100644 --- a/pkg/cmd/ssh/connect_information.go +++ b/pkg/cmd/ssh/connect_information.go @@ -200,6 +200,7 @@ func (p *ConnectInformation) String() string { p.Bastion.UserKnownHostsFiles, nodeHostname, p.NodePrivateKeyFiles, + "", ) fmt.Fprintf(&buf, "> Connect to shoot nodes by using the bastion as a proxy/jump host.\n") diff --git a/pkg/cmd/ssh/export_test.go b/pkg/cmd/ssh/export_test.go index bb638c59..c8012ed8 100644 --- a/pkg/cmd/ssh/export_test.go +++ b/pkg/cmd/ssh/export_test.go @@ -60,6 +60,7 @@ func SSHCommandArguments( bastionUserKnownHostsFiles []string, nodeHostname string, nodePrivateKeyFiles []PrivateKeyFile, + user string, ) TestArguments { return TestArguments{ sshCommandArguments( @@ -69,6 +70,7 @@ func SSHCommandArguments( bastionUserKnownHostsFiles, nodeHostname, nodePrivateKeyFiles, + user, ), } } diff --git a/pkg/cmd/ssh/options.go b/pkg/cmd/ssh/options.go index 182137cd..bd8bd212 100644 --- a/pkg/cmd/ssh/options.go +++ b/pkg/cmd/ssh/options.go @@ -55,15 +55,12 @@ import ( const ( // SSHBastionUsername is the system username on the bastion host. SSHBastionUsername = "gardener" - // DefaultUsername is the default system username on any of the shoot cluster nodes. + // DefaultUsername is the default Shoot cluster node ssh login username. DefaultUsername = "gardener" // SSHPort is the TCP port on a bastion instance that allows incoming SSH. SSHPort = 22 ) -// SSHNodeUsername is ssh login user name. -var SSHNodeUsername string - // wrappers used for unit tests only. var ( // keepAliveInterval is the interval in which bastions should be given the @@ -197,8 +194,8 @@ type SSHOptions struct { // bastion host, but leave it up to the user to SSH themselves. NodeName string - // SSHUser is the name of the Shoot cluster node ssh login user name - SSHUser string + // User is the name of the Shoot cluster node ssh login username + User string // SSHPublicKeyFile is the full path to the file containing the user's // public SSH key. If not given, gardenctl will create a new temporary keypair. @@ -248,6 +245,7 @@ func NewSSHOptions(ioStreams util.IOStreams) *SSHOptions { SkipAvailabilityCheck: false, NoKeepalive: false, BastionPort: strconv.Itoa(SSHPort), + User: DefaultUsername, } } @@ -264,7 +262,7 @@ func (o *SSHOptions) AddFlags(flagSet *pflag.FlagSet) { flagSet.StringVar(&o.BastionPort, "bastion-port", o.BastionPort, "SSH port of the bastion used for the SSH client command. Defaults to port 22") flagSet.StringSliceVar(&o.BastionUserKnownHostsFiles, "bastion-user-known-hosts-file", o.BastionUserKnownHostsFiles, "Path to a custom known hosts file for the SSH connection to the bastion. This file is used to verify the public keys of remote hosts when establishing a secure connection.") flagSet.BoolVarP(&o.ConfirmAccessRestriction, "confirm-access-restriction", "y", o.ConfirmAccessRestriction, "Bypasses the need for confirmation of any access restrictions. Set this flag only if you are fully aware of the access restrictions.") - flagSet.StringVar(&o.SSHUser, "ssh-user", o.SSHUser, "ssh user is the name of the Shoot cluster node ssh login user name.") + flagSet.StringVar(&o.User, "user", o.User, "user is the name of the Shoot cluster node ssh login user name.") o.Options.AddFlags(flagSet) } @@ -316,12 +314,6 @@ func (o *SSHOptions) Complete(f util.Factory, cmd *cobra.Command, args []string) o.BastionName = name } - if o.SSHUser == "" { - SSHNodeUsername = DefaultUsername - } else { - SSHNodeUsername = o.SSHUser - } - return nil } @@ -380,6 +372,10 @@ func (o *SSHOptions) Validate() error { } } + if o.User == "" { + return errors.New("user must not be empty") + } + content, err := os.ReadFile(o.SSHPublicKeyFile.String()) if err != nil { return fmt.Errorf("invalid SSH public key file: %w", err) @@ -694,6 +690,7 @@ func (o *SSHOptions) Run(f util.Factory) error { o.BastionUserKnownHostsFiles, nodeHostname, nodePrivateKeyFiles, + o.User, ) } @@ -958,6 +955,7 @@ func remoteShell( bastionUserKnownHostsFiles []string, nodeHostname string, nodePrivateKeyFiles []PrivateKeyFile, + user string, ) error { commandArgs := sshCommandArguments( bastionHost, @@ -966,6 +964,7 @@ func remoteShell( bastionUserKnownHostsFiles, nodeHostname, nodePrivateKeyFiles, + user, ) fmt.Fprintf(ioStreams.Out, "> You can open additional SSH sessions by running the following command in a separate terminal:\n\n") diff --git a/pkg/cmd/ssh/ssh_test.go b/pkg/cmd/ssh/ssh_test.go index 6019452f..a6c12356 100644 --- a/pkg/cmd/ssh/ssh_test.go +++ b/pkg/cmd/ssh/ssh_test.go @@ -353,7 +353,7 @@ var _ = Describe("SSH Command", func() { ssh.SSHBastionUsername, bastionIP, ), - fmt.Sprintf("%s@%s", ssh.SSHNodeUsername, nodeHostname), + fmt.Sprintf("%s@%s", options.User, nodeHostname), })) return nil @@ -407,7 +407,7 @@ var _ = Describe("SSH Command", func() { ssh.SSHBastionUsername, bastionIP, ), - fmt.Sprintf("%s@%s", ssh.SSHNodeUsername, nodeName), + fmt.Sprintf("%s@%s", options.User, nodeName), })) return nil From f7bd1c6add359ee3c926151309ff51c8fe09c663 Mon Sep 17 00:00:00 2001 From: Ted Teng Date: Fri, 8 Sep 2023 10:38:32 +0800 Subject: [PATCH 3/5] Per feedback --- pkg/cmd/ssh/connect_information.go | 4 +++- pkg/cmd/ssh/options.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/ssh/connect_information.go b/pkg/cmd/ssh/connect_information.go index aa437be1..cbbf0687 100644 --- a/pkg/cmd/ssh/connect_information.go +++ b/pkg/cmd/ssh/connect_information.go @@ -32,6 +32,8 @@ type ConnectInformation struct { // Nodes is a list of Node objects containing information about the worker nodes. Nodes []Node `json:"nodes"` + // User is the name of the Shoot cluster node ssh login username + User string } var _ fmt.Stringer = &ConnectInformation{} @@ -200,7 +202,7 @@ func (p *ConnectInformation) String() string { p.Bastion.UserKnownHostsFiles, nodeHostname, p.NodePrivateKeyFiles, - "", + p.User, ) fmt.Fprintf(&buf, "> Connect to shoot nodes by using the bastion as a proxy/jump host.\n") diff --git a/pkg/cmd/ssh/options.go b/pkg/cmd/ssh/options.go index bd8bd212..99f807aa 100644 --- a/pkg/cmd/ssh/options.go +++ b/pkg/cmd/ssh/options.go @@ -262,7 +262,7 @@ func (o *SSHOptions) AddFlags(flagSet *pflag.FlagSet) { flagSet.StringVar(&o.BastionPort, "bastion-port", o.BastionPort, "SSH port of the bastion used for the SSH client command. Defaults to port 22") flagSet.StringSliceVar(&o.BastionUserKnownHostsFiles, "bastion-user-known-hosts-file", o.BastionUserKnownHostsFiles, "Path to a custom known hosts file for the SSH connection to the bastion. This file is used to verify the public keys of remote hosts when establishing a secure connection.") flagSet.BoolVarP(&o.ConfirmAccessRestriction, "confirm-access-restriction", "y", o.ConfirmAccessRestriction, "Bypasses the need for confirmation of any access restrictions. Set this flag only if you are fully aware of the access restrictions.") - flagSet.StringVar(&o.User, "user", o.User, "user is the name of the Shoot cluster node ssh login user name.") + flagSet.StringVar(&o.User, "user", o.User, "user is the name of the Shoot cluster node ssh login username.") o.Options.AddFlags(flagSet) } From b7e0b51867ad4b2544e10c10dcbe3f448a23f6b3 Mon Sep 17 00:00:00 2001 From: Ted Teng Date: Fri, 8 Sep 2023 10:46:09 +0800 Subject: [PATCH 4/5] update Doc --- docs/help/gardenctl_ssh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/help/gardenctl_ssh.md b/docs/help/gardenctl_ssh.md index 942dff3f..c7eb10d4 100644 --- a/docs/help/gardenctl_ssh.md +++ b/docs/help/gardenctl_ssh.md @@ -58,7 +58,7 @@ gardenctl ssh --keep-bastion --bastion-name cli-xxxxxxxx --public-key-file /path --seed string target the given seed cluster --shoot string target the given shoot cluster --skip-availability-check Skip checking for SSH bastion host availability. - --user string user is the name of the Shoot cluster node ssh login user name. (default "gardener") + --user string user is the name of the Shoot cluster node ssh login username. (default "gardener") --wait-timeout duration Maximum duration to wait for the bastion to become available. (default 10m0s) ``` From f01801b36d2430e7c1d330c8f55fbe7fa5a485f0 Mon Sep 17 00:00:00 2001 From: Ted Teng Date: Mon, 11 Sep 2023 08:58:54 +0800 Subject: [PATCH 5/5] update connect information output --- pkg/cmd/ssh/connect_information.go | 3 +++ pkg/cmd/ssh/options.go | 1 + 2 files changed, 4 insertions(+) diff --git a/pkg/cmd/ssh/connect_information.go b/pkg/cmd/ssh/connect_information.go index cbbf0687..dc19308a 100644 --- a/pkg/cmd/ssh/connect_information.go +++ b/pkg/cmd/ssh/connect_information.go @@ -32,6 +32,7 @@ type ConnectInformation struct { // Nodes is a list of Node objects containing information about the worker nodes. Nodes []Node `json:"nodes"` + // User is the name of the Shoot cluster node ssh login username User string } @@ -86,6 +87,7 @@ func NewConnectInformation( sshPrivateKeyFile PrivateKeyFile, nodePrivateKeyFiles []PrivateKeyFile, nodes []corev1.Node, + user string, ) (*ConnectInformation, error) { var nodeList []Node @@ -142,6 +144,7 @@ func NewConnectInformation( NodeHostname: nodeHostname, NodePrivateKeyFiles: nodePrivateKeyFiles, Nodes: nodeList, + User: user, }, nil } diff --git a/pkg/cmd/ssh/options.go b/pkg/cmd/ssh/options.go index 99f807aa..aa551ae4 100644 --- a/pkg/cmd/ssh/options.go +++ b/pkg/cmd/ssh/options.go @@ -663,6 +663,7 @@ func (o *SSHOptions) Run(f util.Factory) error { o.SSHPrivateKeyFile, nodePrivateKeyFiles, nodes, + o.User, ) if err != nil { return err