Skip to content

Commit a309127

Browse files
authored
fix: Make Git credentials work again (argoproj-labs#737)
* 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]>
1 parent cdb4428 commit a309127

File tree

10 files changed

+256
-15
lines changed

10 files changed

+256
-15
lines changed

cmd/ask_pass.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
// Taken from https://github.com/argoproj/argo-cd/blob/ae19965ff75fd6ba199914b258d751d6b7ea876c/cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go
4+
// All courtesy to the original authors.
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/argoproj/argo-cd/v2/util/git"
12+
13+
"github.com/spf13/cobra"
14+
"google.golang.org/grpc"
15+
"google.golang.org/grpc/credentials/insecure"
16+
17+
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
18+
"github.com/argoproj/argo-cd/v2/util/errors"
19+
grpc_util "github.com/argoproj/argo-cd/v2/util/grpc"
20+
"github.com/argoproj/argo-cd/v2/util/io"
21+
)
22+
23+
const (
24+
// cliName is the name of the CLI
25+
cliName = "argocd-git-ask-pass"
26+
)
27+
28+
func NewAskPassCommand() *cobra.Command {
29+
var command = cobra.Command{
30+
Use: cliName,
31+
Short: "Argo CD git credential helper",
32+
DisableAutoGenTag: true,
33+
Run: func(c *cobra.Command, args []string) {
34+
ctx := c.Context()
35+
36+
if len(os.Args) != 2 {
37+
errors.CheckError(fmt.Errorf("expected 1 argument, got %d", len(os.Args)-1))
38+
}
39+
nonce := os.Getenv(git.ASKPASS_NONCE_ENV)
40+
if nonce == "" {
41+
errors.CheckError(fmt.Errorf("%s is not set", git.ASKPASS_NONCE_ENV))
42+
}
43+
conn, err := grpc_util.BlockingDial(ctx, "unix", askpass.SocketPath, nil, grpc.WithTransportCredentials(insecure.NewCredentials()))
44+
errors.CheckError(err)
45+
defer io.Close(conn)
46+
client := askpass.NewAskPassServiceClient(conn)
47+
48+
creds, err := client.GetCredentials(ctx, &askpass.CredentialsRequest{Nonce: nonce})
49+
errors.CheckError(err)
50+
switch {
51+
case strings.HasPrefix(os.Args[1], "Username"):
52+
fmt.Println(creds.Username)
53+
case strings.HasPrefix(os.Args[1], "Password"):
54+
fmt.Println(creds.Password)
55+
default:
56+
errors.CheckError(fmt.Errorf("unknown credential type '%s'", os.Args[1]))
57+
}
58+
},
59+
}
60+
61+
return &command
62+
}

cmd/main.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"text/template"
66
"time"
77

8+
"github.com/argoproj-labs/argocd-image-updater/ext/git"
89
"github.com/argoproj-labs/argocd-image-updater/pkg/argocd"
910
"github.com/argoproj-labs/argocd-image-updater/pkg/kube"
1011

@@ -45,6 +46,7 @@ type ImageUpdaterConfig struct {
4546
GitCommitMail string
4647
GitCommitMessage *template.Template
4748
DisableKubeEvents bool
49+
GitCreds git.CredsStore
4850
}
4951

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

6466
func main() {
65-
err := newRootCommand()
67+
var err error
68+
69+
// FIXME(jannfis):
70+
// This is a workaround for supporting the Argo CD askpass implementation.
71+
// When the environment ARGOCD_BINARY_NAME is set to argocd-git-ask-pass,
72+
// we divert from the main path of execution to become a git credentials
73+
// helper.
74+
cmdName := os.Getenv("ARGOCD_BINARY_NAME")
75+
if cmdName == "argocd-git-ask-pass" {
76+
cmd := NewAskPassCommand()
77+
err = cmd.Execute()
78+
} else {
79+
err = newRootCommand()
80+
}
6681
if err != nil {
6782
os.Exit(1)
6883
}

cmd/run.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"github.com/argoproj-labs/argocd-image-updater/pkg/registry"
2020
"github.com/argoproj-labs/argocd-image-updater/pkg/version"
2121

22+
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
23+
2224
"github.com/spf13/cobra"
2325

2426
"golang.org/x/sync/semaphore"
@@ -155,6 +157,23 @@ func newRunCommand() *cobra.Command {
155157
}
156158
}
157159

160+
// Start up the credentials store server
161+
cs := askpass.NewServer()
162+
csErrCh := make(chan error)
163+
go func() {
164+
log.Debugf("Starting askpass server")
165+
csErrCh <- cs.Run(askpass.SocketPath)
166+
}()
167+
168+
// Wait for cred server to be started, just in case
169+
err = <-csErrCh
170+
if err != nil {
171+
log.Errorf("Error running askpass server: %v", err)
172+
return err
173+
}
174+
175+
cfg.GitCreds = cs
176+
158177
// This is our main loop. We leave it only when our health probe server
159178
// returns an error.
160179
for {
@@ -309,6 +328,7 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
309328
GitCommitEmail: cfg.GitCommitMail,
310329
GitCommitMessage: cfg.GitCommitMessage,
311330
DisableKubeEvents: cfg.DisableKubeEvents,
331+
GitCreds: cfg.GitCreds,
312332
}
313333
res := argocd.UpdateApplication(upconf, syncState)
314334
result.NumApplicationsProcessed += 1

ext/git/creds.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,15 @@ type Creds interface {
7777
}
7878

7979
func getGitAskPassEnv(id string) []string {
80+
// TODO(jannfis): This change should go upstream into Argo CD. Calling the
81+
// full path to currently executing binary instead of relying on a binary
82+
// named "argocd" in the PATH has only benefits.
83+
cmd, err := os.Executable()
84+
if err != nil {
85+
return []string{}
86+
}
8087
return []string{
81-
fmt.Sprintf("GIT_ASKPASS=%s", "argocd"),
88+
fmt.Sprintf("GIT_ASKPASS=%s", cmd),
8289
fmt.Sprintf("%s=%s", ASKPASS_NONCE_ENV, id),
8390
"GIT_TERMINAL_PROMPT=0",
8491
"ARGOCD_BINARY_NAME=argocd-git-ask-pass",

ext/git/writer.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package git
22

33
import (
44
"fmt"
5+
"os/exec"
56
"strings"
67

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

9495
// SymRefToBranch retrieves the branch name a symbolic ref points to
9596
func (m *nativeGitClient) SymRefToBranch(symRef string) (string, error) {
96-
output, err := m.runCmd("symbolic-ref", symRef)
97+
output, err := m.runCredentialedCmdWithOutput("remote", "show", "origin")
9798
if err != nil {
98-
return "", fmt.Errorf("could not resolve symbolic ref '%s': %v", symRef, err)
99+
return "", fmt.Errorf("error running git: %v", err)
99100
}
100-
if a := strings.SplitN(output, "refs/heads/", 2); len(a) == 2 {
101-
return a[1], nil
101+
for _, l := range strings.Split(output, "\n") {
102+
l = strings.TrimSpace(l)
103+
if strings.HasPrefix(l, "HEAD branch:") {
104+
b := strings.SplitN(l, ":", 2)
105+
if len(b) == 2 {
106+
return strings.TrimSpace(b[1]), nil
107+
}
108+
}
102109
}
103-
return "", fmt.Errorf("no symbolic ref named '%s' could be found", symRef)
110+
return "", fmt.Errorf("no default branch found in remote")
104111
}
105112

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

117124
return nil
118125
}
126+
127+
// runCredentialedCmdWithOutput is a convenience function to run a git command
128+
// with username/password credentials while supplying command output to the
129+
// caller.
130+
// nolint:unparam
131+
func (m *nativeGitClient) runCredentialedCmdWithOutput(args ...string) (string, error) {
132+
closer, environ, err := m.creds.Environ()
133+
if err != nil {
134+
return "", err
135+
}
136+
defer func() { _ = closer.Close() }()
137+
138+
// If a basic auth header is explicitly set, tell Git to send it to the
139+
// server to force use of basic auth instead of negotiating the auth scheme
140+
for _, e := range environ {
141+
if strings.HasPrefix(e, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
142+
args = append([]string{"--config-env", fmt.Sprintf("http.extraHeader=%s", forceBasicAuthHeaderEnv)}, args...)
143+
}
144+
}
145+
146+
cmd := exec.Command("git", args...)
147+
cmd.Env = append(cmd.Env, environ...)
148+
return m.runCmdOutput(cmd, runOpts{})
149+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
2929
golang.org/x/oauth2 v0.11.0
3030
golang.org/x/sync v0.3.0
31+
google.golang.org/grpc v1.59.0
3132
gopkg.in/yaml.v2 v2.4.0
3233
k8s.io/api v0.26.11
3334
k8s.io/apimachinery v0.26.11
@@ -165,7 +166,6 @@ require (
165166
golang.org/x/tools v0.13.0 // indirect
166167
google.golang.org/appengine v1.6.7 // indirect
167168
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
168-
google.golang.org/grpc v1.59.0 // indirect
169169
google.golang.org/protobuf v1.33.0 // indirect
170170
gopkg.in/inf.v0 v0.9.1 // indirect
171171
gopkg.in/warnings.v0 v0.1.2 // indirect

pkg/argocd/gitcreds.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ package argocd
33
import (
44
"context"
55
"fmt"
6+
"net/url"
67
"strconv"
78
"strings"
89

910
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
11+
"github.com/argoproj/argo-cd/v2/util/cert"
1012
"github.com/argoproj/argo-cd/v2/util/db"
1113
"github.com/argoproj/argo-cd/v2/util/settings"
1214

1315
"github.com/argoproj-labs/argocd-image-updater/ext/git"
1416
"github.com/argoproj-labs/argocd-image-updater/pkg/kube"
17+
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
1518
)
1619

1720
// getGitCredsSource returns git credentials source that loads credentials from the secret or from Argo CD settings
@@ -43,7 +46,69 @@ func getCredsFromArgoCD(wbc *WriteBackConfig, kubeClient *kube.KubernetesClient)
4346
if !repo.HasCredentials() {
4447
return nil, fmt.Errorf("credentials for '%s' are not configured in Argo CD settings", wbc.GitRepo)
4548
}
46-
return repo.GetGitCreds(git.NoopCredsStore{}), nil
49+
creds := GetGitCreds(repo, wbc.GitCreds)
50+
return creds, nil
51+
}
52+
53+
// GetGitCreds returns the credentials from a repository configuration used to authenticate at a Git repository
54+
// This is a slightly modified version of upstream's Repository.GetGitCreds method. We need it so it does not return the upstream type.
55+
// TODO(jannfis): Can be removed once we have the change to the git client's getGitAskPassEnv upstream.
56+
func GetGitCreds(repo *v1alpha1.Repository, store git.CredsStore) git.Creds {
57+
if repo == nil {
58+
return git.NopCreds{}
59+
}
60+
if repo.Password != "" {
61+
return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store, repo.ForceHttpBasicAuth)
62+
}
63+
if repo.SSHPrivateKey != "" {
64+
return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), store, repo.Proxy)
65+
}
66+
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
67+
return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store)
68+
}
69+
if repo.GCPServiceAccountKey != "" {
70+
return git.NewGoogleCloudCreds(repo.GCPServiceAccountKey, store)
71+
}
72+
return git.NopCreds{}
73+
}
74+
75+
// Taken from upstream Argo CD.
76+
// TODO(jannfis): Can be removed once we have the change to the git client's getGitAskPassEnv upstream.
77+
func getCAPath(repoURL string) string {
78+
// For git ssh protocol url without ssh://, url.Parse() will fail to parse.
79+
// However, no warn log is output since ssh scheme url is a possible format.
80+
if ok, _ := git.IsSSHURL(repoURL); ok {
81+
return ""
82+
}
83+
84+
hostname := ""
85+
// url.Parse() will happily parse most things thrown at it. When the URL
86+
// is either https or oci, we use the parsed hostname to retrieve the cert,
87+
// otherwise we'll use the parsed path (OCI repos are often specified as
88+
// hostname, without protocol).
89+
parsedURL, err := url.Parse(repoURL)
90+
if err != nil {
91+
log.Warnf("Could not parse repo URL '%s': %v", repoURL, err)
92+
return ""
93+
}
94+
if parsedURL.Scheme == "https" || parsedURL.Scheme == "oci" {
95+
hostname = parsedURL.Host
96+
} else if parsedURL.Scheme == "" {
97+
hostname = parsedURL.Path
98+
}
99+
100+
if hostname == "" {
101+
log.Warnf("Could not get hostname for repository '%s'", repoURL)
102+
return ""
103+
}
104+
105+
caPath, err := cert.GetCertBundlePathForRepository(hostname)
106+
if err != nil {
107+
log.Warnf("Could not get cert bundle path for repository '%s': %v", repoURL, err)
108+
return ""
109+
}
110+
111+
return caPath
47112
}
48113

49114
// getCredsFromSecret loads repository credentials from secret

0 commit comments

Comments
 (0)