Skip to content
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
20 changes: 15 additions & 5 deletions cmd/argocd/commands/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"io/ioutil"
"os"
"syscall"
"text/tabwriter"
Expand Down Expand Up @@ -39,7 +40,8 @@ func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
// NewRepoAddCommand returns a new instance of an `argocd repo add` command
func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
repo appsv1.Repository
repo appsv1.Repository
sshPrivateKeyPath string
)
var command = &cobra.Command{
Use: "add",
Expand All @@ -50,15 +52,22 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
os.Exit(1)
}
repo.Repo = args[0]
err := git.TestRepo(repo.Repo, repo.Username, repo.Password)
if sshPrivateKeyPath != "" {
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
if err != nil {
log.Fatal(err)
}
repo.SSHPrivateKey = string(keyData)
}
err := git.TestRepo(repo.Repo, repo.Username, repo.Password, repo.SSHPrivateKey)
if err != nil {
if repo.Username != "" && repo.Password != "" {
// if everything was supplied, one of the inputs was definitely bad
if repo.Username != "" && repo.Password != "" || git.IsSshURL(repo.Repo) {
// if everything was supplied or repo URL is SSH url, one of the inputs was definitely bad
log.Fatal(err)
}
// If we can't test the repo, it's probably private. Prompt for credentials and try again.
promptCredentials(&repo)
err = git.TestRepo(repo.Repo, repo.Username, repo.Password)
err = git.TestRepo(repo.Repo, repo.Username, repo.Password, repo.SSHPrivateKey)
}
errors.CheckError(err)
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
Expand All @@ -70,6 +79,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
}
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
command.Flags().StringVar(&sshPrivateKeyPath, "sshPrivateKeyPath", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
return command
}

Expand Down
194 changes: 116 additions & 78 deletions pkg/apis/application/v1alpha1/generated.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pkg/apis/application/v1alpha1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions pkg/apis/application/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,10 @@ type TLSClientConfig struct {

// Repository is a Git repository holding application configurations
type Repository struct {
Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"`
Username string `json:"username" protobuf:"bytes,2,opt,name=username"`
Password string `json:"password" protobuf:"bytes,3,opt,name=password"`
Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"`
Username string `json:"username" protobuf:"bytes,2,opt,name=username"`
Password string `json:"password" protobuf:"bytes,3,opt,name=password"`
SSHPrivateKey string `json:"sshPrivateKey" protobuf:"bytes,4,opt,name=sshPrivateKey"`
}

// RepositoryList is a collection of Repositories.
Expand Down
2 changes: 1 addition & 1 deletion reposerver/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (s *Service) GenerateManifest(c context.Context, q *ManifestRequest) (*Mani
s.repoLock.Lock(appRepoPath)
defer s.repoLock.Unlock(appRepoPath)

err := s.gitClient.CloneOrFetch(q.Repo.Repo, q.Repo.Username, q.Repo.Password, appRepoPath)
err := s.gitClient.CloneOrFetch(q.Repo.Repo, q.Repo.Username, q.Repo.Password, q.Repo.SSHPrivateKey, appRepoPath)
if err != nil {
return nil, err
}
Expand Down
18 changes: 10 additions & 8 deletions server/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (s *Server) Create(ctx context.Context, r *appsv1.Repository) (*appsv1.Repo
shallowCopy := *r
r = &shallowCopy
r.Repo = git.NormalizeGitURL(r.Repo)
err := git.TestRepo(r.Repo, r.Username, r.Password)
err := git.TestRepo(r.Repo, r.Username, r.Password, r.SSHPrivateKey)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -111,7 +111,7 @@ func (s *Server) Get(ctx context.Context, q *RepoQuery) (*appsv1.Repository, err

// Update updates a repository
func (s *Server) Update(ctx context.Context, r *appsv1.Repository) (*appsv1.Repository, error) {
err := git.TestRepo(r.Repo, r.Username, r.Password)
err := git.TestRepo(r.Repo, r.Username, r.Password, r.SSHPrivateKey)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -152,17 +152,19 @@ func repoURLToSecretName(repo string) string {
// repoToStringData converts a repository object to string data for serialization to a secret
func repoToStringData(r *appsv1.Repository) map[string]string {
return map[string]string{
"repository": r.Repo,
"username": r.Username,
"password": r.Password,
"repository": r.Repo,
"username": r.Username,
"password": r.Password,
"sshPrivateKey": r.SSHPrivateKey,
}
}

// secretToRepo converts a secret into a repository object
func secretToRepo(s *apiv1.Secret) *appsv1.Repository {
return &appsv1.Repository{
Repo: string(s.Data["repository"]),
Username: string(s.Data["username"]),
Password: string(s.Data["password"]),
Repo: string(s.Data["repository"]),
Username: string(s.Data["username"]),
Password: string(s.Data["password"]),
SSHPrivateKey: string(s.Data["sshPrivateKey"]),
}
}
2 changes: 1 addition & 1 deletion test/e2e/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func PollUntil(t *testing.T, condition wait.ConditionFunc) {
type FakeGitClient struct {
}

func (c *FakeGitClient) CloneOrFetch(repo string, username string, password string, repoPath string) error {
func (c *FakeGitClient) CloneOrFetch(repo string, username string, password string, sshPrivateKey string, repoPath string) error {
_, err := exec.Command("rm", "-rf", repoPath).Output()
if err != nil {
return err
Expand Down
19 changes: 10 additions & 9 deletions util/git/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package git
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"

Expand All @@ -12,7 +11,7 @@ import (

// Client is a generic git client interface
type Client interface {
CloneOrFetch(url string, username string, password string, repoPath string) error
CloneOrFetch(url string, username string, password string, sshPrivateKey string, repoPath string) error
Checkout(repoPath string, sha string) error
}

Expand All @@ -22,7 +21,7 @@ type NativeGitClient struct {
}

// CloneOrFetch either clone or fetch repository into specified directory path.
func (m *NativeGitClient) CloneOrFetch(repo string, username string, password string, repoPath string) error {
func (m *NativeGitClient) CloneOrFetch(repo string, username string, password string, sshPrivateKey string, repoPath string) error {
var needClone bool
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
needClone = true
Expand All @@ -32,22 +31,24 @@ func (m *NativeGitClient) CloneOrFetch(repo string, username string, password st
_, err = cmd.Output()
needClone = err != nil
}

if needClone {
_, err := exec.Command("rm", "-rf", repoPath).Output()
if err != nil {
return fmt.Errorf("unable to clean repo cache at %s: %v", repoPath, err)
}

repoURL, err := url.ParseRequestURI(repo)
repoURL, env, err := GetGitCommandEnvAndURL(repo, username, password, sshPrivateKey)
if err != nil {
return err
}
repoURLNoPassword := repoURL.String()
log.Infof("Cloning %s to %s", repoURLNoPassword, repoPath)
repoURL.User = url.UserPassword(username, password)
_, err = exec.Command("git", "clone", repoURL.String(), repoPath).Output()

log.Infof("Cloning %s to %s", repo, repoPath)
cmd := exec.Command("git", "clone", repoURL, repoPath)
cmd.Env = env
_, err = cmd.Output()
if err != nil {
return fmt.Errorf("unable to clone repository %s: %v", repoURLNoPassword, err)
return fmt.Errorf("unable to clone repository %s: %v", repo, err)
}
} else {
log.Infof("Fetching %s", repo)
Expand Down
54 changes: 47 additions & 7 deletions util/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package git

import (
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
Expand All @@ -11,20 +12,59 @@ import (

// NormalizeGitURL normalizes a git URL for lookup and storage
func NormalizeGitURL(repo string) string {
Copy link
Copy Markdown
Contributor

@merenbach merenbach Mar 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering about unit tests for this function, too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

repoURL, _ := url.Parse(repo)
repoURL, err := url.Parse(repo)
if err != nil {
return strings.ToLower(repo)
}
return repoURL.String()
}

// IsSshURL returns true is supplied URL is SSH URL
func IsSshURL(url string) bool {
return strings.Index(url, "git@") == 0
Copy link
Copy Markdown
Contributor

@merenbach merenbach Mar 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexmt The URL library for Golang has a URL parser that returns a URL struct where you can access the scheme, user, and other attributes. Wondering if this might help make this function more robust.

Edited to add: maybe Parse (which you use above) is more appropriate if we want to support URLs without a scheme attached.

Copy link
Copy Markdown
Contributor

@merenbach merenbach Mar 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wondering if a unit test could be written for this function.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, URL parser considers SSH URL invalid and returns error. Therefore I had to use strings.Index instead. Unit tests have been implemented.

}

// GetGitCommandOptions returns URL and env options for git operation
func GetGitCommandEnvAndURL(repo, username, password string, sshPrivateKey string) (string, []string, error) {
cmdURL := repo
env := os.Environ()
if IsSshURL(repo) {
if sshPrivateKey != "" {
sshFile, err := ioutil.TempFile("", "")
if err != nil {
return "", nil, err
}
_, err = sshFile.WriteString(sshPrivateKey)
if err != nil {
return "", nil, err
}
err = sshFile.Close()
if err != nil {
return "", nil, err
}
env = append(env, fmt.Sprintf("GIT_SSH_COMMAND=ssh -i %s", sshFile.Name()))
}
} else {
env = append(env, "GIT_ASKPASS=")
repoURL, err := url.ParseRequestURI(repo)
if err != nil {
return "", nil, err
}

repoURL.User = url.UserPassword(username, password)
cmdURL = repoURL.String()
}
return cmdURL, env, nil
}

// TestRepo tests if a repo exists and is accessible with the given credentials
func TestRepo(repo, username, password string) error {
repoURL, err := url.ParseRequestURI(repo)
func TestRepo(repo, username, password string, sshPrivateKey string) error {
repo, env, err := GetGitCommandEnvAndURL(repo, username, password, sshPrivateKey)
if err != nil {
return err
}
repoURL.User = url.UserPassword(username, password)
cmd := exec.Command("git", "ls-remote", repoURL.String(), "HEAD")
env := os.Environ()
env = append(env, "GIT_ASKPASS=")
cmd := exec.Command("git", "ls-remote", repo, "HEAD")

cmd.Env = env
_, err = cmd.Output()
if err != nil {
Expand Down
19 changes: 19 additions & 0 deletions util/git/git_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package git

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestReturnsTrueForSSHUrl(t *testing.T) {
assert.True(t, IsSshURL("git@github.com:test.git"))
}

func TestReturnsFalseForNonSSHUrl(t *testing.T) {
assert.False(t, IsSshURL("https://github.com/test.git"))
}

func TestNormalizeUrl(t *testing.T) {
assert.Equal(t, NormalizeGitURL("git@GITHUB.com:test.git"), "git@github.com:test.git")
}
10 changes: 5 additions & 5 deletions util/git/mocks/Client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.