Skip to content

Commit

Permalink
fix: Make Git credentials work again (argoproj-labs#737)
Browse files Browse the repository at this point in the history
* fix: Make Git credentials work again

Signed-off-by: jannfis <[email protected]>

* Update

Signed-off-by: jannfis <[email protected]>

---------

Signed-off-by: jannfis <[email protected]>
  • Loading branch information
jannfis authored Jun 14, 2024
1 parent cdb4428 commit a309127
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 15 deletions.
62 changes: 62 additions & 0 deletions cmd/ask_pass.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

// Taken from https://github.com/argoproj/argo-cd/blob/ae19965ff75fd6ba199914b258d751d6b7ea876c/cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go
// All courtesy to the original authors.

import (
"fmt"
"os"
"strings"

"github.com/argoproj/argo-cd/v2/util/git"

"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/argoproj/argo-cd/v2/reposerver/askpass"
"github.com/argoproj/argo-cd/v2/util/errors"
grpc_util "github.com/argoproj/argo-cd/v2/util/grpc"
"github.com/argoproj/argo-cd/v2/util/io"
)

const (
// cliName is the name of the CLI
cliName = "argocd-git-ask-pass"
)

func NewAskPassCommand() *cobra.Command {
var command = cobra.Command{
Use: cliName,
Short: "Argo CD git credential helper",
DisableAutoGenTag: true,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

if len(os.Args) != 2 {
errors.CheckError(fmt.Errorf("expected 1 argument, got %d", len(os.Args)-1))
}
nonce := os.Getenv(git.ASKPASS_NONCE_ENV)
if nonce == "" {
errors.CheckError(fmt.Errorf("%s is not set", git.ASKPASS_NONCE_ENV))
}
conn, err := grpc_util.BlockingDial(ctx, "unix", askpass.SocketPath, nil, grpc.WithTransportCredentials(insecure.NewCredentials()))
errors.CheckError(err)
defer io.Close(conn)
client := askpass.NewAskPassServiceClient(conn)

creds, err := client.GetCredentials(ctx, &askpass.CredentialsRequest{Nonce: nonce})
errors.CheckError(err)
switch {
case strings.HasPrefix(os.Args[1], "Username"):
fmt.Println(creds.Username)
case strings.HasPrefix(os.Args[1], "Password"):
fmt.Println(creds.Password)
default:
errors.CheckError(fmt.Errorf("unknown credential type '%s'", os.Args[1]))
}
},
}

return &command
}
17 changes: 16 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"text/template"
"time"

"github.com/argoproj-labs/argocd-image-updater/ext/git"
"github.com/argoproj-labs/argocd-image-updater/pkg/argocd"
"github.com/argoproj-labs/argocd-image-updater/pkg/kube"

Expand Down Expand Up @@ -45,6 +46,7 @@ type ImageUpdaterConfig struct {
GitCommitMail string
GitCommitMessage *template.Template
DisableKubeEvents bool
GitCreds git.CredsStore
}

// newRootCommand implements the root command of argocd-image-updater
Expand All @@ -62,7 +64,20 @@ func newRootCommand() error {
}

func main() {
err := newRootCommand()
var err error

// FIXME(jannfis):
// This is a workaround for supporting the Argo CD askpass implementation.
// When the environment ARGOCD_BINARY_NAME is set to argocd-git-ask-pass,
// we divert from the main path of execution to become a git credentials
// helper.
cmdName := os.Getenv("ARGOCD_BINARY_NAME")
if cmdName == "argocd-git-ask-pass" {
cmd := NewAskPassCommand()
err = cmd.Execute()
} else {
err = newRootCommand()
}
if err != nil {
os.Exit(1)
}
Expand Down
20 changes: 20 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/argoproj-labs/argocd-image-updater/pkg/registry"
"github.com/argoproj-labs/argocd-image-updater/pkg/version"

"github.com/argoproj/argo-cd/v2/reposerver/askpass"

"github.com/spf13/cobra"

"golang.org/x/sync/semaphore"
Expand Down Expand Up @@ -155,6 +157,23 @@ func newRunCommand() *cobra.Command {
}
}

// Start up the credentials store server
cs := askpass.NewServer()
csErrCh := make(chan error)
go func() {
log.Debugf("Starting askpass server")
csErrCh <- cs.Run(askpass.SocketPath)
}()

// Wait for cred server to be started, just in case
err = <-csErrCh
if err != nil {
log.Errorf("Error running askpass server: %v", err)
return err
}

cfg.GitCreds = cs

// This is our main loop. We leave it only when our health probe server
// returns an error.
for {
Expand Down Expand Up @@ -309,6 +328,7 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
GitCommitEmail: cfg.GitCommitMail,
GitCommitMessage: cfg.GitCommitMessage,
DisableKubeEvents: cfg.DisableKubeEvents,
GitCreds: cfg.GitCreds,
}
res := argocd.UpdateApplication(upconf, syncState)
result.NumApplicationsProcessed += 1
Expand Down
9 changes: 8 additions & 1 deletion ext/git/creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,15 @@ type Creds interface {
}

func getGitAskPassEnv(id string) []string {
// TODO(jannfis): This change should go upstream into Argo CD. Calling the
// full path to currently executing binary instead of relying on a binary
// named "argocd" in the PATH has only benefits.
cmd, err := os.Executable()
if err != nil {
return []string{}
}
return []string{
fmt.Sprintf("GIT_ASKPASS=%s", "argocd"),
fmt.Sprintf("GIT_ASKPASS=%s", cmd),
fmt.Sprintf("%s=%s", ASKPASS_NONCE_ENV, id),
"GIT_TERMINAL_PROMPT=0",
"ARGOCD_BINARY_NAME=argocd-git-ask-pass",
Expand Down
41 changes: 36 additions & 5 deletions ext/git/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package git

import (
"fmt"
"os/exec"
"strings"

"github.com/argoproj-labs/argocd-image-updater/pkg/log"
Expand Down Expand Up @@ -93,14 +94,20 @@ func (m *nativeGitClient) Add(path string) error {

// SymRefToBranch retrieves the branch name a symbolic ref points to
func (m *nativeGitClient) SymRefToBranch(symRef string) (string, error) {
output, err := m.runCmd("symbolic-ref", symRef)
output, err := m.runCredentialedCmdWithOutput("remote", "show", "origin")
if err != nil {
return "", fmt.Errorf("could not resolve symbolic ref '%s': %v", symRef, err)
return "", fmt.Errorf("error running git: %v", err)
}
if a := strings.SplitN(output, "refs/heads/", 2); len(a) == 2 {
return a[1], nil
for _, l := range strings.Split(output, "\n") {
l = strings.TrimSpace(l)
if strings.HasPrefix(l, "HEAD branch:") {
b := strings.SplitN(l, ":", 2)
if len(b) == 2 {
return strings.TrimSpace(b[1]), nil
}
}
}
return "", fmt.Errorf("no symbolic ref named '%s' could be found", symRef)
return "", fmt.Errorf("no default branch found in remote")
}

// Config configures username and email address for the repository
Expand All @@ -116,3 +123,27 @@ func (m *nativeGitClient) Config(username string, email string) error {

return nil
}

// runCredentialedCmdWithOutput is a convenience function to run a git command
// with username/password credentials while supplying command output to the
// caller.
// nolint:unparam
func (m *nativeGitClient) runCredentialedCmdWithOutput(args ...string) (string, error) {
closer, environ, err := m.creds.Environ()
if err != nil {
return "", err
}
defer func() { _ = closer.Close() }()

// If a basic auth header is explicitly set, tell Git to send it to the
// server to force use of basic auth instead of negotiating the auth scheme
for _, e := range environ {
if strings.HasPrefix(e, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
args = append([]string{"--config-env", fmt.Sprintf("http.extraHeader=%s", forceBasicAuthHeaderEnv)}, args...)
}
}

cmd := exec.Command("git", args...)
cmd.Env = append(cmd.Env, environ...)
return m.runCmdOutput(cmd, runOpts{})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
golang.org/x/oauth2 v0.11.0
golang.org/x/sync v0.3.0
google.golang.org/grpc v1.59.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.26.11
k8s.io/apimachinery v0.26.11
Expand Down Expand Up @@ -165,7 +166,6 @@ require (
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
Expand Down
67 changes: 66 additions & 1 deletion pkg/argocd/gitcreds.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package argocd
import (
"context"
"fmt"
"net/url"
"strconv"
"strings"

"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/cert"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/settings"

"github.com/argoproj-labs/argocd-image-updater/ext/git"
"github.com/argoproj-labs/argocd-image-updater/pkg/kube"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
)

// getGitCredsSource returns git credentials source that loads credentials from the secret or from Argo CD settings
Expand Down Expand Up @@ -43,7 +46,69 @@ func getCredsFromArgoCD(wbc *WriteBackConfig, kubeClient *kube.KubernetesClient)
if !repo.HasCredentials() {
return nil, fmt.Errorf("credentials for '%s' are not configured in Argo CD settings", wbc.GitRepo)
}
return repo.GetGitCreds(git.NoopCredsStore{}), nil
creds := GetGitCreds(repo, wbc.GitCreds)
return creds, nil
}

// GetGitCreds returns the credentials from a repository configuration used to authenticate at a Git repository
// This is a slightly modified version of upstream's Repository.GetGitCreds method. We need it so it does not return the upstream type.
// TODO(jannfis): Can be removed once we have the change to the git client's getGitAskPassEnv upstream.
func GetGitCreds(repo *v1alpha1.Repository, store git.CredsStore) git.Creds {
if repo == nil {
return git.NopCreds{}
}
if repo.Password != "" {
return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store, repo.ForceHttpBasicAuth)
}
if repo.SSHPrivateKey != "" {
return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), store, repo.Proxy)
}
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store)
}
if repo.GCPServiceAccountKey != "" {
return git.NewGoogleCloudCreds(repo.GCPServiceAccountKey, store)
}
return git.NopCreds{}
}

// Taken from upstream Argo CD.
// TODO(jannfis): Can be removed once we have the change to the git client's getGitAskPassEnv upstream.
func getCAPath(repoURL string) string {
// For git ssh protocol url without ssh://, url.Parse() will fail to parse.
// However, no warn log is output since ssh scheme url is a possible format.
if ok, _ := git.IsSSHURL(repoURL); ok {
return ""
}

hostname := ""
// url.Parse() will happily parse most things thrown at it. When the URL
// is either https or oci, we use the parsed hostname to retrieve the cert,
// otherwise we'll use the parsed path (OCI repos are often specified as
// hostname, without protocol).
parsedURL, err := url.Parse(repoURL)
if err != nil {
log.Warnf("Could not parse repo URL '%s': %v", repoURL, err)
return ""
}
if parsedURL.Scheme == "https" || parsedURL.Scheme == "oci" {
hostname = parsedURL.Host
} else if parsedURL.Scheme == "" {
hostname = parsedURL.Path
}

if hostname == "" {
log.Warnf("Could not get hostname for repository '%s'", repoURL)
return ""
}

caPath, err := cert.GetCertBundlePathForRepository(hostname)
if err != nil {
log.Warnf("Could not get cert bundle path for repository '%s': %v", repoURL, err)
return ""
}

return caPath
}

// getCredsFromSecret loads repository credentials from secret
Expand Down
Loading

0 comments on commit a309127

Please sign in to comment.