Skip to content

Commit

Permalink
GitHub proxy part 6.5: tsh git ssh/clone/config
Browse files Browse the repository at this point in the history
  • Loading branch information
greedy52 committed Dec 12, 2024
1 parent 1cd0ec1 commit fc0459d
Show file tree
Hide file tree
Showing 8 changed files with 825 additions and 4 deletions.
114 changes: 110 additions & 4 deletions tool/tsh/common/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,130 @@
package common

import (
"bytes"
"io"
"net/url"
"os/exec"
"strings"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
)

type gitCommands struct {
list *gitListCommand
login *gitLoginCommand
list *gitListCommand
login *gitLoginCommand
ssh *gitSSHCommand
config *gitConfigCommand
clone *gitCloneCommand
}

func newGitCommands(app *kingpin.Application) gitCommands {
git := app.Command("git", "Git server commands.")
cmds := gitCommands{
login: newGitLoginCommand(git),
list: newGitListCommand(git),
login: newGitLoginCommand(git),
list: newGitListCommand(git),
ssh: newGitSSHCommand(git),
config: newGitConfigCommand(git),
clone: newGitCloneCommand(git),
}

// TODO(greedy52) hide the commands until all basic features are implemented.
git.Hidden()
cmds.login.Hidden()
cmds.list.Hidden()
cmds.config.Hidden()
cmds.clone.Hidden()
return cmds
}

type gitSSHURL struct {
user string
host string
port string
path string
// owner is the first part of the path.
// For GitHub, owner is either the user or the organization that owns the
// repo.
owner string
}

func (g gitSSHURL) isGitHub() bool {
return g.host == "github.com"
}

// parseGitSSHURL parse a Git SSH URL.
//
// Normal Git URL Spec: https://git-scm.com/docs/git-clone#_git_urls
// Example: ssh://example.org/path/to/repo.git
//
// GitHub URL Spec: https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories
// Example: git@github.com:gravitational/teleport.git
func parseGitSSHURL(originalURL string) (*gitSSHURL, error) {
if strings.HasPrefix(originalURL, "ssh://") {
u, err := url.Parse(originalURL)
if err != nil {
return nil, trace.Wrap(err)
}

path := strings.TrimLeft(u.Path, "/")
owner, _, _ := strings.Cut(path, "/")
gitSSHURL := &gitSSHURL{
path: path,
host: u.Hostname(),
port: u.Port(),
owner: owner,
}
if u.User != nil {
gitSSHURL.user = u.User.Username()
}
return gitSSHURL, nil
}

if strings.Contains(originalURL, "@github.com:") {
return parseGitHubSSHURL(originalURL)
}
return nil, trace.BadParameter("unsupported git ssh URL %s", originalURL)
}

func parseGitHubSSHURL(originalURL string) (*gitSSHURL, error) {
user, hostAndMore, ok := strings.Cut(originalURL, "@")
if !ok {
return nil, trace.BadParameter("invalid git ssh URL %s", originalURL)
}
host, path, ok := strings.Cut(hostAndMore, ":")
if !ok {
return nil, trace.BadParameter("invalid git ssh URL %s", originalURL)
}
owner, _, ok := strings.Cut(path, "/")
if !ok {
return nil, trace.BadParameter("invalid git ssh URL %s", originalURL)
}
return &gitSSHURL{
user: user,
host: host,
owner: owner,
path: path,
}, nil
}

func execGitAndCaptureStdout(cf *CLIConf, args ...string) (string, error) {
var bufStd bytes.Buffer
if err := execGitWithStdoutAndStderr(cf, &bufStd, cf.Stderr(), args...); err != nil {
return "", trace.Wrap(err)
}
return strings.TrimSpace(bufStd.String()), nil
}

func execGit(cf *CLIConf, args ...string) error {
return trace.Wrap(execGitWithStdoutAndStderr(cf, cf.Stdout(), cf.Stderr(), args...))
}

func execGitWithStdoutAndStderr(cf *CLIConf, stdout, stderr io.Writer, args ...string) error {
log.Debugf("Executing 'git' with args: %v", args)
cmd := exec.CommandContext(cf.Context, "git", args...)
cmd.Stdin = cf.Stdin()
cmd.Stdout = stdout
cmd.Stderr = stderr
return trace.Wrap(cf.RunCommand(cmd))
}
68 changes: 68 additions & 0 deletions tool/tsh/common/git_clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package common

import (
"fmt"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
)

// gitCloneCommand implements `tsh git clone`.
//
// This command internally executes `git clone` while setting `core.sshcommand`.
type gitCloneCommand struct {
*kingpin.CmdClause

repository string
directory string
}

func newGitCloneCommand(parent *kingpin.CmdClause) *gitCloneCommand {
cmd := &gitCloneCommand{
CmdClause: parent.Command("clone", "Clone a Git repository."),
}

cmd.Arg("repository", "Git URL of the repository to clone.").Required().StringVar(&cmd.repository)
cmd.Arg("directory", "The name of a new directory to clone into.").StringVar(&cmd.directory)
// TODO(greedy52) support passing extra args to git like --branch/--depth.
return cmd
}

func (c *gitCloneCommand) run(cf *CLIConf) error {
u, err := parseGitSSHURL(c.repository)
if err != nil {
return trace.Wrap(err)
}
if !u.isGitHub() {
return trace.BadParameter("not a GitHub repository")
}

sshCommand := makeGitCoreSSHCommand(cf.executablePath, u.owner)
args := []string{
"clone",
"--config", fmt.Sprintf("%s=%s", gitCoreSSHCommand, sshCommand),
c.repository,
}
if c.directory != "" {
args = append(args, c.directory)
}
return trace.Wrap(execGit(cf, args...))
}
114 changes: 114 additions & 0 deletions tool/tsh/common/git_clone_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package common

import (
"context"
"os/exec"
"slices"
"testing"

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)

func TestGitCloneCommand(t *testing.T) {
tests := []struct {
name string
cmd *gitCloneCommand
verifyCommand func(*exec.Cmd) error
checkError require.ErrorAssertionFunc
}{
{
name: "success",
cmd: &gitCloneCommand{
repository: "git@github.com:gravitational/teleport.git",
},
verifyCommand: func(cmd *exec.Cmd) error {
expect := []string{
"git", "clone",
"--config", "core.sshcommand=tsh git ssh --github-org gravitational",
"git@github.com:gravitational/teleport.git",
}
if !slices.Equal(expect, cmd.Args) {
return trace.CompareFailed("expect %v but got %v", expect, cmd.Args)
}
return nil
},
checkError: require.NoError,
},
{
name: "success with target dir",
cmd: &gitCloneCommand{
repository: "git@github.com:gravitational/teleport.git",
directory: "target_dir",
},
verifyCommand: func(cmd *exec.Cmd) error {
expect := []string{
"git", "clone",
"--config", "core.sshcommand=tsh git ssh --github-org gravitational",
"git@github.com:gravitational/teleport.git",
"target_dir",
}
if !slices.Equal(expect, cmd.Args) {
return trace.CompareFailed("expect %v but got %v", expect, cmd.Args)
}
return nil
},
checkError: require.NoError,
},
{
name: "invalid URL",
cmd: &gitCloneCommand{
repository: "not-a-git-ssh-url",
},
checkError: require.Error,
},
{
name: "unsupported Git service",
cmd: &gitCloneCommand{
repository: "git@gitlab.com:group/project.git",
},
checkError: require.Error,
},
{
name: "git fails",
cmd: &gitCloneCommand{
repository: "git@github.com:gravitational/teleport.git",
},
verifyCommand: func(cmd *exec.Cmd) error {
return trace.BadParameter("some git error")
},
checkError: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorIs(t, err, trace.BadParameter("some git error"))
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cf := &CLIConf{
Context: context.Background(),
executablePath: "tsh",
cmdRunner: tt.verifyCommand,
}
tt.checkError(t, tt.cmd.run(cf))
})
}
}
Loading

0 comments on commit fc0459d

Please sign in to comment.