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: Make Git credentials work again #737

Merged
merged 2 commits into from
Jun 14, 2024
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
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)
Dismissed Show dismissed Hide dismissed
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
Loading