diff --git a/docs/daytona_ssh.md b/docs/daytona_ssh.md index b921d3aa51..fc8d57bb3b 100644 --- a/docs/daytona_ssh.md +++ b/docs/daytona_ssh.md @@ -9,7 +9,8 @@ daytona ssh [WORKSPACE] [PROJECT] [CMD...] [flags] ### Options ``` - -y, --yes Automatically confirm any prompts + -o, --option stringArray Specify SSH options in KEY=VALUE format. + -y, --yes Automatically confirm any prompts ``` ### Options inherited from parent commands diff --git a/hack/docs/daytona_ssh.yaml b/hack/docs/daytona_ssh.yaml index e068bd2d65..968f854992 100644 --- a/hack/docs/daytona_ssh.yaml +++ b/hack/docs/daytona_ssh.yaml @@ -2,6 +2,10 @@ name: daytona ssh synopsis: SSH into a project using the terminal usage: daytona ssh [WORKSPACE] [PROJECT] [CMD...] [flags] options: + - name: option + shorthand: o + default_value: '[]' + usage: Specify SSH options in KEY=VALUE format. - name: "yes" shorthand: "y" default_value: "false" diff --git a/pkg/cmd/workspace/code.go b/pkg/cmd/workspace/code.go index 7cc73b66cf..aeb337d1b1 100644 --- a/pkg/cmd/workspace/code.go +++ b/pkg/cmd/workspace/code.go @@ -177,7 +177,7 @@ func openIDE(ideId string, activeProfile config.Profile, workspaceId string, pro case "vscode": return ide.OpenVSCode(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) case "ssh": - return ide.OpenTerminalSsh(activeProfile, workspaceId, projectName, gpgKey) + return ide.OpenTerminalSsh(activeProfile, workspaceId, projectName, gpgKey, nil) case "browser": return ide.OpenBrowserIDE(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) case "cursor": diff --git a/pkg/cmd/workspace/ssh.go b/pkg/cmd/workspace/ssh.go index dd8e3dbc34..853db71d54 100644 --- a/pkg/cmd/workspace/ssh.go +++ b/pkg/cmd/workspace/ssh.go @@ -18,6 +18,8 @@ import ( "github.com/spf13/cobra" ) +var sshOptions []string + var SshCmd = &cobra.Command{ Use: "ssh [WORKSPACE] [PROJECT] [CMD...]", Short: "SSH into a project using the terminal", @@ -103,7 +105,7 @@ var SshCmd = &cobra.Command{ log.Warn(err) } - return ide.OpenTerminalSsh(activeProfile, workspace.Id, projectName, gpgKey, sshArgs...) + return ide.OpenTerminalSsh(activeProfile, workspace.Id, projectName, gpgKey, sshOptions, sshArgs...) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) >= 2 { @@ -119,4 +121,5 @@ var SshCmd = &cobra.Command{ func init() { SshCmd.Flags().BoolVarP(&yesFlag, "yes", "y", false, "Automatically confirm any prompts") + SshCmd.Flags().StringArrayVarP(&sshOptions, "option", "o", []string{}, "Specify SSH options in KEY=VALUE format.") } diff --git a/pkg/ide/terminal.go b/pkg/ide/terminal.go index ec2bf3aea4..3acbaa19ff 100644 --- a/pkg/ide/terminal.go +++ b/pkg/ide/terminal.go @@ -4,22 +4,27 @@ package ide import ( + "fmt" "os" "os/exec" + "strings" "github.com/daytonaio/daytona/cmd/daytona/config" ) -func OpenTerminalSsh(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string, args ...string) error { - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey) +func OpenTerminalSsh(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string, sshOptions []string, args ...string) error { + if err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey); err != nil { + return err + } + + // Parse SSH options + parsedOptions, err := parseSshOptions(sshOptions) if err != nil { return err } projectHostname := config.GetProjectHostname(activeProfile.Id, workspaceId, projectName) - - cmdArgs := []string{projectHostname} - cmdArgs = append(cmdArgs, args...) + cmdArgs := buildCommandArgs(projectHostname, parsedOptions, args...) sshCommand := exec.Command("ssh", cmdArgs...) sshCommand.Stdin = os.Stdin @@ -28,3 +33,27 @@ func OpenTerminalSsh(activeProfile config.Profile, workspaceId string, projectNa return sshCommand.Run() } + +// parseSshOptions validates and parses the SSH options. +func parseSshOptions(sshOptions []string) (map[string]string, error) { + parsedOptions := make(map[string]string) + for _, option := range sshOptions { + parts := strings.SplitN(option, "=", 2) + if len(parts) == 1 { + return nil, fmt.Errorf("no argument after keyword %q", parts[0]) + } + if len(parts) != 2 || strings.Count(option, "=") > 1 { + return nil, fmt.Errorf("bad configuration option: %s", option) + } + parsedOptions[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + return parsedOptions, nil +} + +func buildCommandArgs(projectHostname string, parsedOptions map[string]string, args ...string) []string { + cmdArgs := []string{projectHostname} + for key, value := range parsedOptions { + cmdArgs = append(cmdArgs, "-o", fmt.Sprintf("%s=%s", key, value)) + } + return append(cmdArgs, args...) +}