From 7d93c7ab1521e0928764eb8c7bb5b27656b051a2 Mon Sep 17 00:00:00 2001 From: Dustin Lactin Date: Thu, 7 Dec 2023 11:03:44 -0700 Subject: [PATCH] feat: add support for separate GitHub app credentials (#649) * feat: add support for separate GitHub app credentials stored as Kubernetes secrets Signed-off-by: Dustin Lactin * test: added tests for consuming GitHub app credentials from a secret Signed-off-by: Dustin Lactin * fix: added GitHub App placeholder words to expect.txt Signed-off-by: Dustin Lactin * fix: checking for errors when converting GitHub App and Installation IDs Signed-off-by: Dustin Lactin * fix: added more descriptive error messages for string-to-number conversions Signed-off-by: Dustin Lactin --------- Signed-off-by: Dustin Lactin --- .github/actions/spelling/expect.txt | 3 ++ docs/basics/update-methods.md | 14 ++++++++- pkg/argocd/gitcreds.go | 32 +++++++++++++++----- pkg/argocd/update_test.go | 45 +++++++++++++++++++++++++++-- 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index b37e7f3d..642a1bc4 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,6 +1,7 @@ aeece Artifactory +applicationid bacd CVE credref @@ -11,9 +12,11 @@ fbd ffb gitlab helmvalues +installationid jfrog mep myregistry +PRIVATEKEYDATA repocreds rollbacked someimage diff --git a/docs/basics/update-methods.md b/docs/basics/update-methods.md index e64947ad..404b5aa4 100644 --- a/docs/basics/update-methods.md +++ b/docs/basics/update-methods.md @@ -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.: @@ -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 diff --git a/pkg/argocd/gitcreds.go b/pkg/argocd/gitcreds.go index d7996026..46336cf6 100644 --- a/pkg/argocd/gitcreds.go +++ b/pkg/argocd/gitcreds.go @@ -3,6 +3,7 @@ package argocd import ( "context" "fmt" + "strconv" "strings" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" @@ -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") } diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go index b0c345a4..1d1246fa 100644 --- a/pkg/argocd/update_test.go +++ b/pkg/argocd/update_test.go @@ -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{ @@ -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)