Skip to content

Commit

Permalink
feat: Allow reuse of Argo CD repo credentials (#141)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Matyushentsev <[email protected]>
  • Loading branch information
Alexander Matyushentsev authored Jan 22, 2021
1 parent c0d7e1d commit 74e9d3f
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 170 deletions.
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ mypass
otherapp
wildcard
wildcards
credref
repocreds
18 changes: 12 additions & 6 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate
var kubeClient *kube.KubernetesClient
var err error
if kubeConfig != "" {
kubeClient, err = getKubeConfig(ctx, kubeConfig)
kubeClient, err = getKubeConfig(ctx, "", kubeConfig)
if err != nil {
log.Fatalf("could not create K8s client: %v", err)
}
Expand Down Expand Up @@ -406,10 +406,16 @@ func newRunCommand() *cobra.Command {
var err error
if !disableKubernetes {
ctx := context.Background()
cfg.KubeClient, err = getKubeConfig(ctx, kubeConfig)
cfg.KubeClient, err = getKubeConfig(ctx, cfg.ArgocdNamespace, kubeConfig)
if err != nil {
log.Fatalf("could not create K8s client: %v", err)
}
if cfg.ClientOpts.ServerAddr == "" {
cfg.ClientOpts.ServerAddr = fmt.Sprintf("argocd-server.%s", cfg.KubeClient.Namespace)
}
}
if cfg.ClientOpts.ServerAddr == "" {
cfg.ClientOpts.ServerAddr = defaultArgoCDServerAddr
}

if token := os.Getenv("ARGOCD_TOKEN"); token != "" && cfg.ClientOpts.AuthToken == "" {
Expand Down Expand Up @@ -490,7 +496,7 @@ func newRunCommand() *cobra.Command {
},
}

runCmd.Flags().StringVar(&cfg.ClientOpts.ServerAddr, "argocd-server-addr", env.GetStringVal("ARGOCD_SERVER", defaultArgoCDServerAddr), "address of ArgoCD API server")
runCmd.Flags().StringVar(&cfg.ClientOpts.ServerAddr, "argocd-server-addr", env.GetStringVal("ARGOCD_SERVER", ""), "address of ArgoCD API server")
runCmd.Flags().BoolVar(&cfg.ClientOpts.GRPCWeb, "argocd-grpc-web", env.GetBoolVal("ARGOCD_GRPC_WEB", false), "use grpc-web for connection to ArgoCD")
runCmd.Flags().BoolVar(&cfg.ClientOpts.Insecure, "argocd-insecure", env.GetBoolVal("ARGOCD_INSECURE", false), "(INSECURE) ignore invalid TLS certs for ArgoCD server")
runCmd.Flags().BoolVar(&cfg.ClientOpts.Plaintext, "argocd-plaintext", env.GetBoolVal("ARGOCD_PLAINTEXT", false), "(INSECURE) connect without TLS to ArgoCD server")
Expand All @@ -505,14 +511,14 @@ func newRunCommand() *cobra.Command {
runCmd.Flags().StringVar(&cfg.RegistriesConf, "registries-conf-path", defaultRegistriesConfPath, "path to registries configuration file")
runCmd.Flags().BoolVar(&disableKubernetes, "disable-kubernetes", false, "do not create and use a Kubernetes client")
runCmd.Flags().IntVar(&cfg.MaxConcurrency, "max-concurrency", 10, "maximum number of update threads to run concurrently")
runCmd.Flags().StringVar(&cfg.ArgocdNamespace, "argocd-namespace", "argocd", "namespace where ArgoCD runs in")
runCmd.Flags().StringVar(&cfg.ArgocdNamespace, "argocd-namespace", "", "namespace where ArgoCD runs in (current namespace by default)")
runCmd.Flags().StringSliceVar(&cfg.AppNamePatterns, "match-application-name", nil, "patterns to match application name against")
runCmd.Flags().BoolVar(&warmUpCache, "warmup-cache", true, "whether to perform a cache warm-up on startup")

return runCmd
}

func getKubeConfig(ctx context.Context, kubeConfig string) (*kube.KubernetesClient, error) {
func getKubeConfig(ctx context.Context, namespace string, kubeConfig string) (*kube.KubernetesClient, error) {
var fullKubeConfigPath string
var kubeClient *kube.KubernetesClient
var err error
Expand All @@ -530,7 +536,7 @@ func getKubeConfig(ctx context.Context, kubeConfig string) (*kube.KubernetesClie
log.Debugf("Creating in-cluster Kubernetes client")
}

kubeClient, err = kube.NewKubernetesClientFromConfig(ctx, fullKubeConfigPath)
kubeClient, err = kube.NewKubernetesClientFromConfig(ctx, namespace, fullKubeConfigPath)
if err != nil {
return nil, err
}
Expand Down
22 changes: 11 additions & 11 deletions docs/configuration/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,21 +160,21 @@ Configuration for the Git write-back method comes from two sources:

#### Specifying Git credentials

In order for Argo CD Image Updater to be able to push any changes back to your
Git repository (e.g. hosted on GitHub, GitLab or elsewhere), you will need to
configure credentials that have write access to your remote upstream repository.
Argo CD Image Updater will **not** re-use the credentials you have configured
By default Argo CD Image Updater re-uses the credentials you have configured
in Argo CD for accessing the repository.

Credentials must be stored in a Kubernetes secret, which needs to be accessible
by the Argo CD Image Updater's Service Account. The secret can be configured
using the `argocd-image-updater.argoproj.io/git-credentials` annotation, whose
value must be in format `namespace/secret-name`, for example to use a secret
named `git-creds` in the namespace `argocd-image-updater`, use following
annotation:
If you don't want to use credentials configured for Argo CD you can use other credentials stored in a Kubernetes secret,
which needs to be accessible by the Argo CD Image Updater's Service Account. The secret should be specified in
`argocd-image-updater.argoproj.io/write-back-method` annotation using `git:<credref>` format. Where `<credref>` might
take one of following values:

* `repocreds` (default) - Git repository credentials configured in Argo CD settings
* `secret:<namespace>/<secret>` - namespace and secret name.

Example:

```yaml
argocd-image-updater.argoproj.io/git-credentials: argocd-image-updater/git-creds
argocd-image-updater.argoproj.io/write-back-method: git:secret:argocd-image-updater/git-creds
```

If the repository is accessed using HTTPS, the secret must contain two fields:
Expand Down
1 change: 1 addition & 0 deletions manifests/base/rbac/argocd-image-updater-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rules:
- ''
resources:
- secrets
- configmaps
verbs:
- get
- list
Expand Down
1 change: 1 addition & 0 deletions manifests/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rules:
- ""
resources:
- secrets
- configmaps
verbs:
- get
- list
Expand Down
79 changes: 79 additions & 0 deletions pkg/argocd/gitcreds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package argocd

import (
"context"
"fmt"
"strings"

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

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

// getGitCredsSource returns git credentials source that loads credentials from the secret or from Argo CD settings
func getGitCredsSource(creds string, kubeClient *kube.KubernetesClient) (GitCredsSource, error) {
switch {
case creds == "repocreds":
return func(app *v1alpha1.Application) (git.Creds, error) {
return getCredsFromArgoCD(app, kubeClient)
}, nil
case strings.HasPrefix(creds, "secret:"):
return func(app *v1alpha1.Application) (git.Creds, error) {
return getCredsFromSecret(app, creds[len("secret:"):], kubeClient)
}, nil
}
return nil, fmt.Errorf("unexpected credentials format. Expected 'repocreds' or 'secret:<namespace>/<secret>' but got '%s'", creds)
}

// getCredsFromArgoCD loads repository credentials from Argo CD settings
func getCredsFromArgoCD(app *v1alpha1.Application, kubeClient *kube.KubernetesClient) (git.Creds, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

settingsMgr := settings.NewSettingsManager(ctx, kubeClient.Clientset, kubeClient.Namespace)
argocdDB := db.NewDB(kubeClient.Namespace, settingsMgr, kubeClient.Clientset)
repo, err := argocdDB.GetRepository(ctx, app.Spec.Source.RepoURL)
if err != nil {
return nil, err
}
if !repo.HasCredentials() {
return nil, fmt.Errorf("credentials for '%s' are not configured in Argo CD settings", app.Spec.Source.RepoURL)
}
return repo.GetGitCreds(), nil
}

// getCredsFromSecret loads repository credentials from secret
func getCredsFromSecret(app *v1alpha1.Application, credentialsSecret string, kubeClient *kube.KubernetesClient) (git.Creds, error) {
var credentials map[string][]byte
var err error
s := strings.SplitN(credentialsSecret, "/", 2)
if len(s) == 2 {
credentials, err = kubeClient.GetSecretData(s[0], s[1])
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("secret ref must be in format 'namespace/name', but is '%s'", credentialsSecret)
}

if ok, _ := git.IsSSHURL(app.Spec.Source.RepoURL); ok {
var sshPrivateKey []byte
if sshPrivateKey, ok = credentials["sshPrivateKey"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field sshPrivateKey", credentialsSecret)
}
return git.NewSSHCreds(string(sshPrivateKey), "", true), nil
} else if git.IsHTTPSURL(app.Spec.Source.RepoURL) {
var username, password []byte
if username, ok = credentials["username"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field username", credentialsSecret)
}
if password, ok = credentials["password"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field password", credentialsSecret)
}
return git.NewHTTPSCreds(string(username), string(password), "", "", true), nil
}
return nil, fmt.Errorf("unknown repository type")
}
66 changes: 18 additions & 48 deletions pkg/argocd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type ImageUpdaterResult struct {
NumErrors int
}

type GitCredsSource func(app *v1alpha1.Application) (git.Creds, error)

type WriteBackMethod int

const (
Expand All @@ -43,9 +45,9 @@ type WriteBackConfig struct {
Method WriteBackMethod
ArgoClient ArgoCD
// If GitClient is not nil, the client will be used for updates. Otherwise, a new client will be created.
GitClient git.Client
KubeClient *kube.KubernetesClient
GitBranch string
GitClient git.Client
GetCreds GitCredsSource
GitBranch string
}

// The following are helper structs to only marshal the fields we require
Expand Down Expand Up @@ -271,14 +273,20 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl
wbc := &WriteBackConfig{}
// Default write-back is to use Argo CD API
wbc.Method = WriteBackApplication
wbc.KubeClient = kubeClient
wbc.ArgoClient = argoClient

// If we have no update method, just return our default
method, ok := app.Annotations[common.WriteBackMethodAnnotation]
if !ok || strings.TrimSpace(method) == "argocd" {
return wbc, nil
}
method = strings.TrimSpace(method)

creds := "repocreds"
if index := strings.Index(method, ":"); index > 0 {
creds = method[index+1:]
method = method[:index]
}

// We might support further methods later
switch strings.TrimSpace(method) {
Expand All @@ -288,56 +296,18 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl
if ok {
wbc.GitBranch = strings.TrimSpace(branch)
}
credsSource, err := getGitCredsSource(creds, kubeClient)
if err != nil {
return nil, fmt.Errorf("invalid git credentials source: %v", err)
}
wbc.GetCreds = credsSource
default:
return nil, fmt.Errorf("invalid update mechanism: %s", method)
}

return wbc, nil
}

// getGitCreds looks at a secret ref in application's annotations and constructs
// a Creds object for git client, according to the repository URL in the app
// spec.
func getGitCreds(app *v1alpha1.Application, wbc *WriteBackConfig) (git.Creds, error) {
var credentials map[string][]byte
var ok bool
var credentialsSecret string
var err error

if credentialsSecret, ok = app.Annotations[common.GitCredentialsAnnotation]; ok {
s := strings.SplitN(credentialsSecret, "/", 2)
if len(s) == 2 {
credentials, err = wbc.KubeClient.GetSecretData(s[0], s[1])
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("secret ref must be in format 'namespace/name', but is '%s'", credentialsSecret)
}
} else {
return nil, fmt.Errorf("no secret ref annotation %s found", common.GitCredentialsAnnotation)
}

if ok, _ := git.IsSSHURL(app.Spec.Source.RepoURL); ok {
var sshPrivateKey []byte
if sshPrivateKey, ok = credentials["sshPrivateKey"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field sshPrivateKey", credentialsSecret)
}
return git.NewSSHCreds(string(sshPrivateKey), "", true), nil
} else if git.IsHTTPSURL(app.Spec.Source.RepoURL) {
var username, password []byte
if username, ok = credentials["username"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field username", credentialsSecret)
}
if password, ok = credentials["password"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field password", credentialsSecret)
}
return git.NewHTTPSCreds(string(username), string(password), "", "", true), nil
}

return nil, fmt.Errorf("unknown repository type")
}

// commitChanges commits any changes required for updating one or more images
// after the UpdateApplication cycle has finished.
func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig) error {
Expand All @@ -351,7 +321,7 @@ func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig) error {
return err
}
case WriteBackGit:
creds, err := getGitCreds(app, wbc)
creds, err := wbc.GetCreds(app)
if err != nil {
return fmt.Errorf("could not get creds for repo '%s': %v", app.Spec.Source.RepoURL, err)
}
Expand Down
Loading

0 comments on commit 74e9d3f

Please sign in to comment.