Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli): ssh based auth with external key #1208

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions cmd/agent/workspace/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ func prepareWorkspace(ctx context.Context, workspaceInfo *provider2.AgentWorkspa
}

log.Debugf("Clone Repository")
gitCloner := git.NewCloner(workspaceInfo.CLIOptions.GitCloneStrategy)
err = CloneRepository(ctx, workspaceInfo.CLIOptions.SSHKey, workspaceInfo.Agent.Local == "true", workspaceInfo.ContentFolder, workspaceInfo.Workspace.Source, helper, gitCloner, log)
err = CloneRepository(ctx, workspaceInfo, helper, log)
if err != nil {
// fallback
log.Errorf("Cloning failed: %v. Trying cloning on local machine and uploading folder", err)
Expand Down Expand Up @@ -414,7 +413,8 @@ func (cmd *UpCmd) devPodUp(ctx context.Context, workspaceInfo *provider2.AgentWo
return result, nil
}

func CloneRepository(ctx context.Context, sshkey string, local bool, workspaceDir string, source provider2.WorkspaceSource, helper string, cloner git.Cloner, log log.Logger) error {
func CloneRepository(ctx context.Context, workspaceInfo *provider2.AgentWorkspaceInfo, helper string, log log.Logger) error {
workspaceDir := workspaceInfo.ContentFolder
// remove the credential helper or otherwise we will receive strange errors within the container
defer func() {
if helper != "" {
Expand All @@ -432,6 +432,7 @@ func CloneRepository(ctx context.Context, sshkey string, local bool, workspaceDi

// check if command exists
if !command.Exists("git") {
local, _ := workspaceInfo.Agent.Local.Bool()
if local {
return fmt.Errorf("seems like git isn't installed on your system. Please make sure to install git and make it available in the PATH")
}
Expand Down Expand Up @@ -485,18 +486,19 @@ func CloneRepository(ctx context.Context, sshkey string, local bool, workspaceDi
}

// run git command
gitInfo := git.NewGitInfo(source.GitRepository, source.GitBranch, source.GitCommit, source.GitPRReference, source.GitSubPath)
s := workspaceInfo.Workspace.Source
gitInfo := git.NewGitInfo(s.GitRepository, s.GitBranch, s.GitCommit, s.GitPRReference, s.GitSubPath)
extraEnv := []string{}

if sshkey != "" {
if workspaceInfo.CLIOptions.SSHKey != "" {
key, err := os.CreateTemp("", "")
if err != nil {
return err
}
defer os.Remove(key.Name())
defer key.Close()

err = writeSSHKey(key, sshkey)
err = writeSSHKey(key, workspaceInfo.CLIOptions.SSHKey)
if err != nil {
return err
}
Expand All @@ -507,9 +509,11 @@ func CloneRepository(ctx context.Context, sshkey string, local bool, workspaceDi
}

extraEnv = append(extraEnv, "GIT_TERMINAL_PROMPT=0")
extraEnv = append(extraEnv, "GIT_SSH_COMMAND=ssh -oBatchMode=yes -o IdentitiesOnly=yes -oStrictHostKeyChecking=no -o IdentityFile="+key.Name())
gitSSHCmd := []string{workspaceInfo.Agent.Path, "helper", "ssh-git-clone", "--key-file=" + key.Name()}
extraEnv = append(extraEnv, "GIT_SSH_COMMAND="+command.Quote(gitSSHCmd))
}

cloner := git.NewCloner(workspaceInfo.CLIOptions.GitCloneStrategy)
err := git.CloneRepositoryWithEnv(ctx, gitInfo, extraEnv, workspaceDir, helper, false, cloner, writer, log)
if err != nil {
return errors.Wrap(err, "clone repository")
Expand Down
1 change: 1 addition & 0 deletions cmd/helper/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewHelperCmd(globalFlags *flags.GlobalFlags) *cobra.Command {
helperCmd.AddCommand(NewCheckProviderUpdateCmd(globalFlags))
helperCmd.AddCommand(NewSSHClientCmd())
helperCmd.AddCommand(NewShellCmd())
helperCmd.AddCommand(NewSSHGitCloneCmd())
helperCmd.AddCommand(NewFleetServerCmd(globalFlags))
helperCmd.AddCommand(NewDockerCredentialsHelperCmd(globalFlags))
return helperCmd
Expand Down
105 changes: 105 additions & 0 deletions cmd/helper/ssh_git_clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package helper

import (
"context"
"fmt"
"net"
"os"
"strings"

command2 "github.com/loft-sh/devpod/pkg/command"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
)

type SSHGitClone struct {
KeyFile string
Port string
}

func NewSSHGitCloneCmd() *cobra.Command {
cmd := &SSHGitClone{}
sshCmd := &cobra.Command{
Use: "ssh-git-clone",
Short: "Drop-in ssh replacement in GIT_SSH_COMMAND",
RunE: func(_ *cobra.Command, args []string) error {
return cmd.Run(context.Background(), args)
},
}

sshCmd.Flags().StringVar(&cmd.KeyFile, "key-file", "", "SSH Key file to use")
sshCmd.Flags().StringVar(&cmd.Port, "port", "22", "SSH port to use, defaults to 22")
_ = sshCmd.MarkFlagRequired("key-file")
return sshCmd
}

func (cmd *SSHGitClone) Run(ctx context.Context, args []string) error {
if len(args) < 2 {
return fmt.Errorf("expected args in format: {user}@{host} {commands...}, received \"%s\"", strings.Join(args, " "))
}
host := args[0]
sshCmdArgs := args[1:]
if len(host) == 0 || len(sshCmdArgs) == 0 {
return fmt.Errorf("unexpected input: host: %s, args: %s", host, strings.Join(sshCmdArgs, " "))
}

user, addr, err := parseSSHHost(host)
if err != nil {
return err
}

sshConfig, err := getConfig(user, cmd.KeyFile)
if err != nil {
return err
}

sshClient, err := ssh.Dial("tcp", net.JoinHostPort(addr, cmd.Port), sshConfig)
if err != nil {
return err
}
defer sshClient.Close()

sess, err := sshClient.NewSession()
if err != nil {
return err
}
defer sess.Close()

sess.Stdin = os.Stdin
sess.Stdout = os.Stdout
sess.Stderr = os.Stderr
err = sess.Run(command2.Quote(sshCmdArgs))
if err != nil {
return err
}

return nil
}

func getConfig(userName string, keyFilePath string) (*ssh.ClientConfig, error) {
out, err := os.ReadFile(keyFilePath)
if err != nil {
return nil, fmt.Errorf("read private ssh key: %w", err)
}

signer, err := ssh.ParsePrivateKey(out)
if err != nil {
return nil, fmt.Errorf("parse private key: %w", err)
}

return &ssh.ClientConfig{
User: userName,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}, nil
}

func parseSSHHost(host string) (string, string, error) {
s := strings.SplitN(host, "@", 2)
if len(s) != 2 {
return "", "", fmt.Errorf("split host: %s", host)
}

return s[0], s[1], nil
}
14 changes: 11 additions & 3 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,31 @@ func (l *LifecycleHook) UnmarshalJSON(data []byte) error {
type StrBool string

// UnmarshalJSON parses fields that may be numbers or booleans.
func (f *StrBool) UnmarshalJSON(data []byte) error {
func (s *StrBool) UnmarshalJSON(data []byte) error {
var jsonObj interface{}
err := json.Unmarshal(data, &jsonObj)
if err != nil {
return err
}
switch obj := jsonObj.(type) {
case string:
*f = StrBool(obj)
*s = StrBool(obj)
return nil
case bool:
*f = StrBool(strconv.FormatBool(obj))
*s = StrBool(strconv.FormatBool(obj))
return nil
}
return ErrUnsupportedType
}

func (s *StrBool) Bool() (bool, error) {
if s == nil {
return false, nil
}

return strconv.ParseBool(string(*s))
}

type OptionEnum struct {
Value string `json:"value,omitempty"`
DisplayName string `json:"displayName,omitempty"`
Expand Down
Loading