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

feat: add support for separate GitHub app credentials #649

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

aeece
Artifactory
applicationid
bacd
CVE
credref
Expand All @@ -11,9 +12,11 @@ fbd
ffb
gitlab
helmvalues
installationid
jfrog
mep
myregistry
PRIVATEKEYDATA
repocreds
rollbacked
someimage
Expand Down
14 changes: 13 additions & 1 deletion docs/basics/update-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ Example:
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:
If the repository is accessed using HTTPS, the secret must contain either user credentials or GitHub app credentials.

If the repository is accessed using user credentials, the secret requires two fields
`username` which holds the Git username, and `password` which holds the user's
password or a private access token (PAT) with write access to the repository.
You can generate such a secret using `kubectl`, e.g.:
Expand All @@ -134,6 +136,16 @@ kubectl -n argocd-image-updater create secret generic git-creds \
--from-literal=password=somepassword
```

If the repository is accessed using GitHub app credentials, the secret requires three fields `githubAppID` which holds the GitHub Application ID, `githubAppInstallationID` which holds the GitHub Organization Installation ID, and `githubAppPrivateKey` which holds the GitHub Application private key. The GitHub Application must be installed into the target repository with write access.
You can generate such a secret using `kubectl`, e.g.:

```bash
kubectl -n argocd-image-updater create secret generic git-creds \
--from-literal=githubAppID=applicationid \
--from-literal=githubAppInstallationID=installationid \
--from-literal=githubAppPrivateKey='-----BEGIN RSA PRIVATE KEY-----PRIVATEKEYDATA-----END RSA PRIVATE KEY-----'
```

If the repository is accessed using SSH, the secret must contain the field
`sshPrivateKey`, which holds a SSH private key in OpenSSH-compatible PEM
format. To create such a secret from an existing private key, you can use
Expand Down
26 changes: 19 additions & 7 deletions pkg/argocd/gitcreds.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package argocd
import (
"context"
"fmt"
"strconv"
"strings"

"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
Expand Down Expand Up @@ -66,14 +67,25 @@ func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, kubeClie
}
return git.NewSSHCreds(string(sshPrivateKey), "", true), nil
} else if git.IsHTTPSURL(wbc.GitRepo) {
var username, password []byte
if username, ok = credentials["username"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field username", credentialsSecret)
var username, password, githubAppID, githubAppInstallationID, githubAppPrivateKey []byte
if githubAppID, ok = credentials["githubAppID"]; ok {
if githubAppInstallationID, ok = credentials["githubAppInstallationID"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field githubAppInstallationID", credentialsSecret)
}
if githubAppPrivateKey, ok = credentials["githubAppPrivateKey"]; !ok {
return nil, fmt.Errorf("invalid secret %s: does not contain field githubAppPrivateKey", credentialsSecret)
}
// converting byte array to string and ultimately int64 for NewGitHubAppCreds
intGithubAppID, _ := strconv.ParseInt(string(githubAppID), 10, 64)
intGithubAppInstallationID, _ := strconv.ParseInt(string(githubAppInstallationID), 10, 64)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we really should check for error in string-to-number conversion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, meant to add that after testing. I'll add it now!

return git.NewGitHubAppCreds(intGithubAppID, intGithubAppInstallationID, string(githubAppPrivateKey), "", "", "", "", true), nil
} else if username, ok = credentials["username"]; ok {
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
}
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("invalid repository credentials in secret %s: does not contain githubAppID or username", credentialsSecret)
}
return nil, fmt.Errorf("unknown repository type")
}
45 changes: 43 additions & 2 deletions pkg/argocd/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2055,7 +2055,7 @@ func Test_GetWriteBackConfig(t *testing.T) {
}

func Test_GetGitCreds(t *testing.T) {
t.Run("HTTP creds from a secret", func(t *testing.T) {
t.Run("HTTP user creds from a secret", func(t *testing.T) {
argoClient := argomock.ArgoCD{}
argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil)
secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{
Expand Down Expand Up @@ -2090,11 +2090,52 @@ func Test_GetGitCreds(t *testing.T) {
creds, err := wbc.GetCreds(&app)
require.NoError(t, err)
require.NotNil(t, creds)
// Must have HTTPS creds
// Must have HTTPS user creds
_, ok := creds.(git.HTTPSCreds)
require.True(t, ok)
})

t.Run("HTTP GitHub App creds from a secret", func(t *testing.T) {
argoClient := argomock.ArgoCD{}
argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil)
secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{
"githubAppID": []byte("12345678"),
"githubAppInstallationID": []byte("87654321"),
"githubAppPrivateKey": []byte("foo"),
})
kubeClient := kube.KubernetesClient{
Clientset: fake.NewFakeClientsetWithResources(secret),
}
app := v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Name: "testapp",
Annotations: map[string]string{
"argocd-image-updater.argoproj.io/image-list": "nginx",
"argocd-image-updater.argoproj.io/write-back-method": "git:secret:argocd-image-updater/git-creds",
"argocd-image-updater.argoproj.io/git-credentials": "argocd-image-updater/git-creds",
},
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/example",
TargetRevision: "main",
},
},
Status: v1alpha1.ApplicationStatus{
SourceType: v1alpha1.ApplicationSourceTypeKustomize,
},
}
wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient)
require.NoError(t, err)

creds, err := wbc.GetCreds(&app)
require.NoError(t, err)
require.NotNil(t, creds)
// Must have HTTPS GitHub App creds
_, ok := creds.(git.GitHubAppCreds)
require.True(t, ok)
})

t.Run("SSH creds from a secret", func(t *testing.T) {
argoClient := argomock.ArgoCD{}
argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil)
Expand Down
Loading