From 048073293f3341aef4f233399571d49a43754415 Mon Sep 17 00:00:00 2001 From: Ivan Mikheykin Date: Sat, 11 Apr 2026 17:50:23 +0300 Subject: [PATCH 1/3] fix(cli): mute warnings during cobra command configuration - Move home dir detection into RunE function. - Cleanup flags descriptions. - Build d8v in current dir, instead of a hidden directory. - More examples for ssh command. Signed-off-by: Ivan Mikheykin --- src/cli/Taskfile.yaml | 7 +- src/cli/internal/cmd/scp/scp.go | 12 ++- src/cli/internal/cmd/ssh/ssh.go | 103 ++++++++++++++++++-------- src/cli/internal/cmd/ssh/wrapped.go | 6 +- src/cli/pkg/command/virtualization.go | 8 +- 5 files changed, 94 insertions(+), 42 deletions(-) diff --git a/src/cli/Taskfile.yaml b/src/cli/Taskfile.yaml index 37d1afd565..7e1e7b9010 100644 --- a/src/cli/Taskfile.yaml +++ b/src/cli/Taskfile.yaml @@ -8,7 +8,8 @@ tasks: build: desc: "Build d8v cli" cmds: - - go build -o .out/d8v cmd/main.go + - echo Build binary ./d8v ... + - go build -o ./d8v cmd/main.go install: desc: "Install d8v cli to ~/.local/bin" deps: [build] @@ -16,12 +17,12 @@ tasks: - echo "Check that ~/.local/bin in your PATH" - echo "Installing d8v to ~/.local/bin" - mkdir -p ~/.local/bin - - cp .out/d8v ~/.local/bin/d8v + - cp .d8v ~/.local/bin/d8v - task: clean clean: desc: "Clean up build artifacts" cmds: - - rm -rf .out + - test -f ./d8v && rm -rf ./d8v lint: desc: "Run linters locally" diff --git a/src/cli/internal/cmd/scp/scp.go b/src/cli/internal/cmd/scp/scp.go index 345c648c71..fe9b91ac0e 100644 --- a/src/cli/internal/cmd/scp/scp.go +++ b/src/cli/internal/cmd/scp/scp.go @@ -39,14 +39,15 @@ func NewCommand() *cobra.Command { c.options.LocalClientName = "scp" cmd := &cobra.Command{ - Use: "scp VirtualMachine)", - Short: "SCP files from/to a virtual machine.", + Use: "scp [flags] SOURCE TARGET", + Short: "Secure file CoPy from/to a virtual machine.", Example: usage(), Args: templates.ExactArgs("scp", 2), RunE: c.Run, } - ssh.AddCommandlineArgs(cmd.Flags(), &c.options) + ssh.AddCommonSSHFlags(cmd.Flags(), &c.options) + cmd.Flags().BoolVarP(&c.recursive, recursiveFlag, recursiveFlagShort, c.recursive, "Recursively copy entire directories") cmd.Flags().BoolVar(&c.preserve, preserveFlag, c.preserve, @@ -62,6 +63,11 @@ type SCP struct { } func (o *SCP) Run(cmd *cobra.Command, args []string) error { + err := o.options.ResolvePaths() + if err != nil { + return err + } + client, defaultNamespace, _, err := clientconfig.ClientAndNamespaceFromContext(cmd.Context()) if err != nil { return err diff --git a/src/cli/internal/cmd/ssh/ssh.go b/src/cli/internal/cmd/ssh/ssh.go index d4f2c81d18..d7a00acd0c 100644 --- a/src/cli/internal/cmd/ssh/ssh.go +++ b/src/cli/internal/cmd/ssh/ssh.go @@ -23,10 +23,10 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" - "k8s.io/klog/v2" "github.com/deckhouse/virtualization/src/cli/internal/clientconfig" "github.com/deckhouse/virtualization/src/cli/internal/templates" @@ -62,28 +62,52 @@ type SSHOptions struct { } func DefaultSSHOptions() SSHOptions { - homeDir, err := os.UserHomeDir() - if err != nil { - klog.Warningf("failed to determine user home directory: %v", err) - } + options := SSHOptions{ SSHPort: 22, SSHUsername: defaultUsername(), - IdentityFilePath: filepath.Join(homeDir, ".ssh", "id_rsa"), + IdentityFilePath: filepath.Join("~", ".ssh", "id_rsa"), IdentityFilePathProvided: false, KnownHostsFilePath: "", - KnownHostsFilePathDefault: "", + KnownHostsFilePathDefault: filepath.Join("~", ".ssh", KnownHostsFileName), AdditionalSSHLocalOptions: []string{}, WrapLocalSSH: wrapLocalSSHDefault, LocalClientName: "ssh", } - if len(homeDir) > 0 { - options.KnownHostsFilePathDefault = filepath.Join(homeDir, ".ssh", KnownHostsFileName) - } return options } +func (s *SSHOptions) ResolvePaths() error { + if s.IdentityFilePath != "" { + resolvedPath, err := resolveHomeDir(s.IdentityFilePath) + if err != nil { + return fmt.Errorf("resolve identity file path '%s': %w", s.IdentityFilePath, err) + } + s.IdentityFilePath = resolvedPath + } + if s.KnownHostsFilePath != "" { + resolvedPath, err := resolveHomeDir(s.KnownHostsFilePath) + if err != nil { + return fmt.Errorf("resolve known hosts file path '%s': %w", s.KnownHostsFilePath, err) + } + s.KnownHostsFilePath = resolvedPath + } + return nil +} + +// resolveHomeDir substitutes beginning '~' with home dir path. +func resolveHomeDir(path string) (string, error) { + if !strings.HasPrefix(path, "~") { + return path, nil + } + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("get user home directory: %w", err) + } + return filepath.Join(homeDir, strings.TrimPrefix(path, "~")), nil +} + func defaultUsername() string { vars := []string{ "USER", // linux @@ -104,34 +128,40 @@ func NewCommand() *cobra.Command { } cmd := &cobra.Command{ - Use: "ssh VirtualMachine", + Use: "ssh [-n|--namespace NAMESPACE] VIRTUAL-MACHINE-NAME", Short: "Open a SSH connection to a virtual machine.", Example: usage(), Args: templates.ExactArgs("ssh", 1), RunE: c.Run, } - AddCommandlineArgs(cmd.Flags(), &c.options) + AddCommonSSHFlags(cmd.Flags(), &c.options) + + cmd.Flags().StringVarP(&c.options.SSHUsername, usernameFlag, usernameFlagShort, c.options.SSHUsername, + "Specify user to log into virtual machine; If unassigned, this will be empty and the SSH default will apply") cmd.Flags().StringVarP(&c.command, commandToExecute, commandToExecuteShort, c.command, - fmt.Sprintf(`--%s='ls /': Specify a command to execute in the VM`, commandToExecute)) + "Specify a command to execute in the VM.") cmd.SetUsageTemplate(templates.UsageTemplate()) return cmd } -func AddCommandlineArgs(flagset *pflag.FlagSet, opts *SSHOptions) { - flagset.StringVarP(&opts.SSHUsername, usernameFlag, usernameFlagShort, opts.SSHUsername, - fmt.Sprintf("--%s=%s: Set this to the user you want to open the SSH connection as; If unassigned, this will be empty and the SSH default will apply", usernameFlag, opts.SSHUsername)) +func AddCommonSSHFlags(flagset *pflag.FlagSet, opts *SSHOptions) { flagset.StringVarP(&opts.IdentityFilePath, IdentityFilePathFlag, identityFilePathFlagShort, opts.IdentityFilePath, - fmt.Sprintf("--%s=/home/user/.ssh/id_rsa: Set the path to a private key used for authenticating to the server; If not provided, the client will try to use the local ssh-agent at $SSH_AUTH_SOCK", IdentityFilePathFlag)) + "Specify a path to a private key used for authenticating to the server; If not provided, the client will try to use the local ssh-agent at $SSH_AUTH_SOCK") flagset.StringVar(&opts.KnownHostsFilePath, knownHostsFilePathFlag, opts.KnownHostsFilePathDefault, - fmt.Sprintf("--%s=/home/user/.ssh/%s: Set the path to the known_hosts file.", KnownHostsFileName, knownHostsFilePathFlag)) + "Set a path to the known_hosts file.") flagset.IntVarP(&opts.SSHPort, portFlag, portFlagShort, opts.SSHPort, - fmt.Sprintf(`--%s=22: Specify a port on the VM to send SSH traffic to`, portFlag)) + `Specify a port to connect to`) - addAdditionalCommandlineArgs(flagset, opts) + addLocalSSHClientFlags(flagset, opts) } func (o *SSH) Run(cmd *cobra.Command, args []string) error { + err := o.options.ResolvePaths() + if err != nil { + return err + } + client, defaultNamespace, _, err := clientconfig.ClientAndNamespaceFromContext(cmd.Context()) if err != nil { return err @@ -168,19 +198,30 @@ func PrepareCommand(cmd *cobra.Command, defaultNamespace string, opts *SSHOption } func usage() string { - return fmt.Sprintf(` # Connect to 'myvm': - {{ProgramName}} ssh user@myvm [--%s] + return fmt.Sprintf(` # Connect to virtualMachine 'myvm' in 'vms' namespace: + {{ProgramName}} ssh user@myvm.vms + + # Specify namespace and user with flags: + {{ProgramName}} ssh --namespace=vms --%s=user myvm + + # Specify identity file: + {{ProgramName}} ssh -n vms user@myvm -%s /some/path/id_rsa_for_myvm - # Connect to 'myvm' in 'mynamespace' namespace - {{ProgramName}} ssh user@myvm.mynamespace [--%s] + # Run command instead of opening shell: + {{ProgramName}} ssh -n vms user@myvm -%s 'ls -la /' - # Specify a username and namespace: - {{ProgramName}} ssh --namespace=mynamespace --%s=user myvm + # Connect using the local ssh binary found in $PATH: + {{ProgramName}} ssh --%s=true user@myvm - # Connect to 'myvm' using the local ssh binary found in $PATH: - {{ProgramName}} ssh --%s=true user@myvm`, - IdentityFilePathFlag, - IdentityFilePathFlag, + # Specify additional options for local ssh: + {{ProgramName}} ssh user@myvm --%s=true --%s='-o StrictHostKeyChecking=no' --%s='-o UserKnownHostsFile=/dev/null' +`, usernameFlag, - wrapLocalSSHFlag) + identityFilePathFlagShort, + commandToExecuteShort, + wrapLocalSSHFlag, + wrapLocalSSHFlag, + additionalOpts, + additionalOpts, + ) } diff --git a/src/cli/internal/cmd/ssh/wrapped.go b/src/cli/internal/cmd/ssh/wrapped.go index 846e24a119..78b1d0c108 100644 --- a/src/cli/internal/cmd/ssh/wrapped.go +++ b/src/cli/internal/cmd/ssh/wrapped.go @@ -31,11 +31,11 @@ import ( "k8s.io/klog/v2" ) -func addAdditionalCommandlineArgs(flagset *pflag.FlagSet, opts *SSHOptions) { +func addLocalSSHClientFlags(flagset *pflag.FlagSet, opts *SSHOptions) { flagset.StringArrayVarP(&opts.AdditionalSSHLocalOptions, additionalOpts, additionalOptsShort, opts.AdditionalSSHLocalOptions, - fmt.Sprintf(`--%s="-o StrictHostKeyChecking=no" : Additional options to be passed to the local ssh. This is applied only if local-ssh=true`, additionalOpts)) + "Additional options to be passed to the ssh client if --local-ssh=true is set") flagset.BoolVar(&opts.WrapLocalSSH, wrapLocalSSHFlag, opts.WrapLocalSSH, - fmt.Sprintf("--%s=true: Set this to true to use the SSH/SCP client available on your system by using this command as ProxyCommand; If set to false, this will establish a SSH/SCP connection with limited capabilities provided by this client", wrapLocalSSHFlag)) + "Use the SSH/SCP client available on your system by using this command as ProxyCommand; Default is false: use embedded SSH client with limited capabilities") } func RunLocalClient(cmd *cobra.Command, namespace, name string, options *SSHOptions, clientArgs []string) error { diff --git a/src/cli/pkg/command/virtualization.go b/src/cli/pkg/command/virtualization.go index 7115c3f8f0..1ccbfd944a 100644 --- a/src/cli/pkg/command/virtualization.go +++ b/src/cli/pkg/command/virtualization.go @@ -21,6 +21,7 @@ package command import ( "context" + "fmt" "os" "os/signal" "strings" @@ -43,6 +44,9 @@ import ( "github.com/deckhouse/virtualization/src/cli/internal/templates" ) +// NewCommand defines command and flags configuration for cobra. +// Warning: d8 cli calls all configurations for all commands, so +// do not print warnings or errors here, postpone such actions to runtime (RunE). func NewCommand(programName string) *cobra.Command { // programName used in cobra templates to display either `d8 virtualization` or `d8vctl` cobra.AddTemplateFunc( @@ -60,8 +64,8 @@ func NewCommand(programName string) *cobra.Command { ) virtCmd := &cobra.Command{ - Use: programName, - Short: programName + " controls virtual machine related operations on your kubernetes cluster.", + Use: fmt.Sprintf("%s command [options]", programName), + Short: "Commands to work with Deckhouse Virtualization Platform.", SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { From 2d17a089a6a5eba368dac070c517e3d95c53201f Mon Sep 17 00:00:00 2001 From: Ivan Mikheykin Date: Sat, 11 Apr 2026 19:19:31 +0300 Subject: [PATCH 2/3] ++ add Usage and Aliases to main usage template Signed-off-by: Ivan Mikheykin --- src/cli/internal/templates/templates.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cli/internal/templates/templates.go b/src/cli/internal/templates/templates.go index b48e5cb5c5..a3f170b164 100644 --- a/src/cli/internal/templates/templates.go +++ b/src/cli/internal/templates/templates.go @@ -56,7 +56,13 @@ Use "{{ProgramName}} options" for a list of global command-line options (applies // MainUsageTemplate returns the usage template for the root command func MainUsageTemplate() string { - return `Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + return `Usage:{{if .Runnable}} + {{prepare .UseLine}}{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} {{rpad .Name .NamePadding }} {{prepare .Short}}{{end}}{{end}} Use "{{ProgramName}} --help" for more information about a given command. From 511d83a97fc182de96431e2e2ea46991e88b8551 Mon Sep 17 00:00:00 2001 From: Ivan Mikheykin Date: Sat, 11 Apr 2026 19:59:04 +0300 Subject: [PATCH 3/3] ++ lint Signed-off-by: Ivan Mikheykin --- src/cli/Taskfile.yaml | 2 +- src/cli/internal/cmd/ssh/ssh.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli/Taskfile.yaml b/src/cli/Taskfile.yaml index 7e1e7b9010..fa49212a5b 100644 --- a/src/cli/Taskfile.yaml +++ b/src/cli/Taskfile.yaml @@ -17,7 +17,7 @@ tasks: - echo "Check that ~/.local/bin in your PATH" - echo "Installing d8v to ~/.local/bin" - mkdir -p ~/.local/bin - - cp .d8v ~/.local/bin/d8v + - cp ./d8v ~/.local/bin/d8v - task: clean clean: desc: "Clean up build artifacts" diff --git a/src/cli/internal/cmd/ssh/ssh.go b/src/cli/internal/cmd/ssh/ssh.go index d7a00acd0c..acc5d72b1e 100644 --- a/src/cli/internal/cmd/ssh/ssh.go +++ b/src/cli/internal/cmd/ssh/ssh.go @@ -62,7 +62,6 @@ type SSHOptions struct { } func DefaultSSHOptions() SSHOptions { - options := SSHOptions{ SSHPort: 22, SSHUsername: defaultUsername(),