Skip to content

Commit

Permalink
feat: add support for separate GitHub app credentials (#649)
Browse files Browse the repository at this point in the history
* feat: add support for separate GitHub app credentials stored as Kubernetes secrets

Signed-off-by: Dustin Lactin <dustin.lactin@gmail.com>

* test: added tests for consuming GitHub app credentials from a secret

Signed-off-by: Dustin Lactin <dustin.lactin@gmail.com>

* fix: added GitHub App placeholder words to expect.txt

Signed-off-by: Dustin Lactin <dustin.lactin@gmail.com>

* fix: checking for errors when converting GitHub App and Installation IDs

Signed-off-by: Dustin Lactin <dustin.lactin@gmail.com>

* fix: added more descriptive error messages for string-to-number conversions

Signed-off-by: Dustin Lactin <dustin.lactin@gmail.com>

---------

Signed-off-by: Dustin Lactin <dustin.lactin@gmail.com>
  • Loading branch information
dlactin committed Dec 7, 2023
1 parent 0252dae commit 7d93c7a
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 10 deletions.
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
32 changes: 25 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,31 @@ 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, err := strconv.ParseInt(string(githubAppID), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid value in field githubAppID: %w", err)
}
intGithubAppInstallationID, _ := strconv.ParseInt(string(githubAppInstallationID), 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid value in field githubAppInstallationID: %w", err)
}
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

0 comments on commit 7d93c7a

Please sign in to comment.