Skip to content

Commit

Permalink
feat(applicationset): reuse repo-creds for an existing GitHub App (#1…
Browse files Browse the repository at this point in the history
…0092)

Closes #10079

Signed-off-by: Noah Perks Sloan <[email protected]>

Signed-off-by: Noah Perks Sloan <[email protected]>
  • Loading branch information
iamnoah authored Aug 19, 2022
1 parent d545198 commit 506bd3b
Show file tree
Hide file tree
Showing 21 changed files with 337 additions and 23 deletions.
32 changes: 24 additions & 8 deletions applicationset/generators/pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gosimple/slug"

"github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
pullrequest "github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/gosimple/slug"
)

var _ Generator = (*PullRequestGenerator)(nil)
Expand All @@ -24,11 +25,13 @@ const (
type PullRequestGenerator struct {
client client.Client
selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
auth SCMAuthProviders
}

func NewPullRequestGenerator(client client.Client) Generator {
func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders) Generator {
g := &PullRequestGenerator{
client: client,
auth: auth,
}
g.selectServiceProviderFunc = g.selectServiceProvider
return g
Expand Down Expand Up @@ -101,12 +104,7 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
// selectServiceProvider selects the provider to get pull requests from the configuration
func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
if generatorConfig.Github != nil {
providerConfig := generatorConfig.Github
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %v", err)
}
return pullrequest.NewGithubService(ctx, token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Labels)
return g.github(ctx, generatorConfig.Github, applicationSetInfo)
}
if generatorConfig.GitLab != nil {
providerConfig := generatorConfig.GitLab
Expand Down Expand Up @@ -139,6 +137,24 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
return nil, fmt.Errorf("no Pull Request provider implementation configured")
}

func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alpha1.PullRequestGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
// use an app if it was configured
if cfg.AppSecretName != "" {
auth, err := g.auth.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName)
if err != nil {
return nil, fmt.Errorf("error getting GitHub App secret: %v", err)
}
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
}

// always default to token, even if not set (public access)
token, err := g.getSecretRef(ctx, cfg.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %v", err)
}
return pullrequest.NewGithubService(ctx, token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
}

// getSecretRef gets the value of the key for the specified Secret resource.
func (g *PullRequestGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
if ref == nil {
Expand Down
44 changes: 36 additions & 8 deletions applicationset/generators/scm_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
Expand All @@ -24,10 +25,18 @@ type SCMProviderGenerator struct {
client client.Client
// Testing hooks.
overrideProvider scm_provider.SCMProviderService
SCMAuthProviders
}

func NewSCMProviderGenerator(client client.Client) Generator {
return &SCMProviderGenerator{client: client}
type SCMAuthProviders struct {
GitHubApps github_app_auth.Credentials
}

func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders) Generator {
return &SCMProviderGenerator{
client: client,
SCMAuthProviders: providers,
}
}

// Testing generator
Expand Down Expand Up @@ -66,13 +75,10 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
if g.overrideProvider != nil {
provider = g.overrideProvider
} else if providerConfig.Github != nil {
token, err := g.getSecretRef(ctx, providerConfig.Github.TokenRef, applicationSetInfo.Namespace)
var err error
provider, err = g.githubProvider(ctx, providerConfig.Github, applicationSetInfo)
if err != nil {
return nil, fmt.Errorf("error fetching Github token: %v", err)
}
provider, err = scm_provider.NewGithubProvider(ctx, providerConfig.Github.Organization, token, providerConfig.Github.API, providerConfig.Github.AllBranches)
if err != nil {
return nil, fmt.Errorf("error initializing Github service: %v", err)
return nil, fmt.Errorf("scm provider: %w", err)
}
} else if providerConfig.Gitlab != nil {
token, err := g.getSecretRef(ctx, providerConfig.Gitlab.TokenRef, applicationSetInfo.Namespace)
Expand Down Expand Up @@ -169,3 +175,25 @@ func (g *SCMProviderGenerator) getSecretRef(ctx context.Context, ref *argoprojio
}
return string(tokenBytes), nil
}

func (g *SCMProviderGenerator) githubProvider(ctx context.Context, github *argoprojiov1alpha1.SCMProviderGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (scm_provider.SCMProviderService, error) {
if github.AppSecretName != "" {
auth, err := g.GitHubApps.GetAuthSecret(ctx, github.AppSecretName)
if err != nil {
return nil, fmt.Errorf("error fetching Github app secret: %v", err)
}

return scm_provider.NewGithubAppProviderFor(
*auth,
github.Organization,
github.API,
github.AllBranches,
)
}

token, err := g.getSecretRef(ctx, github.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Github token: %v", err)
}
return scm_provider.NewGithubProvider(ctx, github.Organization, token, github.API, github.AllBranches)
}
19 changes: 19 additions & 0 deletions applicationset/services/github_app_auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package github_app_auth

import "context"

// Authentication has the authentication information required to access the GitHub API and repositories.
type Authentication struct {
// Id specifies the ID of the GitHub app used to access the repo
Id int64
// InstallationId specifies the installation ID of the GitHub App used to access the repo
InstallationId int64
// EnterpriseBaseURL specifies the base URL of GitHub Enterprise installation. If empty will default to https://api.github.com
EnterpriseBaseURL string
// PrivateKey in PEM format.
PrivateKey string
}

type Credentials interface {
GetAuthSecret(ctx context.Context, secretName string) (*Authentication, error)
}
33 changes: 33 additions & 0 deletions applicationset/services/internal/github_app/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package github_app

import (
"fmt"
"net/http"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v35/github"

"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
)

// Client builds a github client for the given app authentication.
func Client(g github_app_auth.Authentication, url string) (*github.Client, error) {
rt, err := ghinstallation.New(http.DefaultTransport, g.Id, g.InstallationId, []byte(g.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to create github app install: %w", err)
}
if url == "" {
url = g.EnterpriseBaseURL
}
var client *github.Client
httpClient := http.Client{Transport: rt}
if url == "" {
client = github.NewClient(&httpClient)
} else {
client, err = github.NewEnterpriseClient(url, url, &httpClient)
if err != nil {
return nil, fmt.Errorf("failed to create github enterprise client: %w", err)
}
}
return client, nil
}
2 changes: 1 addition & 1 deletion applicationset/services/pull_request/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (g *GithubService) List(ctx context.Context) ([]*PullRequest, error) {
for {
pulls, resp, err := g.client.PullRequests.List(ctx, g.owner, g.repo, opts)
if err != nil {
return nil, fmt.Errorf("error listing pull requests for %s/%s: %v", g.owner, g.repo, err)
return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", g.owner, g.repo, err)
}
for _, pull := range pulls {
if !containLabels(g.labels, pull.Labels) {
Expand Down
19 changes: 19 additions & 0 deletions applicationset/services/pull_request/github_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package pull_request

import (
"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/applicationset/services/internal/github_app"
)

func NewGithubAppService(g github_app_auth.Authentication, url, owner, repo string, labels []string) (PullRequestService, error) {
client, err := github_app.Client(g, url)
if err != nil {
return nil, err
}
return &GithubService{
client: client,
owner: owner,
repo: repo,
labels: labels,
}, nil
}
4 changes: 2 additions & 2 deletions applicationset/services/scm_provider/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (g *GithubProvider) GetBranches(ctx context.Context, repo *Repository) ([]*
repos := []*Repository{}
branches, err := g.listBranches(ctx, repo)
if err != nil {
return nil, fmt.Errorf("error listing branches for %s/%s: %v", repo.Organization, repo.Repository, err)
return nil, fmt.Errorf("error listing branches for %s/%s: %w", repo.Organization, repo.Repository, err)
}

for _, branch := range branches {
Expand All @@ -72,7 +72,7 @@ func (g *GithubProvider) ListRepos(ctx context.Context, cloneProtocol string) ([
for {
githubRepos, resp, err := g.client.Repositories.ListByOrg(ctx, g.organization, opt)
if err != nil {
return nil, fmt.Errorf("error listing repositories for %s: %v", g.organization, err)
return nil, fmt.Errorf("error listing repositories for %s: %w", g.organization, err)
}
for _, githubRepo := range githubRepos {
var url string
Expand Down
14 changes: 14 additions & 0 deletions applicationset/services/scm_provider/github_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package scm_provider

import (
"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/applicationset/services/internal/github_app"
)

func NewGithubAppProviderFor(g github_app_auth.Authentication, organization string, url string, allBranches bool) (*GithubProvider, error) {
client, err := github_app.Client(g, url)
if err != nil {
return nil, err
}
return &GithubProvider{client: client, organization: organization, allBranches: allBranches}, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
"github.com/argoproj/argo-cd/v2/util/env"
"github.com/argoproj/argo-cd/v2/util/github_app"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -136,13 +137,16 @@ func NewCommand() *cobra.Command {
argoCDDB := db.NewDB(namespace, argoSettingsMgr, k8sClient)

askPassServer := askpass.NewServer()
scmAuth := generators.SCMAuthProviders{
GitHubApps: github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB)),
}
terminalGenerators := map[string]generators.Generator{
"List": generators.NewListGenerator(),
"Clusters": generators.NewClusterGenerator(mgr.GetClient(), ctx, k8sClient, namespace),
"Git": generators.NewGitGenerator(services.NewArgoCDService(argoCDDB, askPassServer, getSubmoduleEnabled())),
"SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient()),
"SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient(), scmAuth),
"ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace),
"PullRequest": generators.NewPullRequestGenerator(mgr.GetClient()),
"PullRequest": generators.NewPullRequestGenerator(mgr.GetClient(), scmAuth),
}

nestedGenerators := map[string]generators.Generator{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ spec:
tokenRef:
secretName: github-token
key: token
# (optional) use a GitHub App to access the API instead of a PAT.
appSecretName: github-app-repo-creds
# Labels is used to filter the PRs that you want to target. (optional)
labels:
- preview
Expand All @@ -57,6 +59,9 @@ spec:
* `api`: If using GitHub Enterprise, the URL to access it. (Optional)
* `tokenRef`: A `Secret` name and key containing the GitHub access token to use for requests. If not specified, will make anonymous requests which have a lower rate limit and can only see public repositories. (Optional)
* `labels`: Labels is used to filter the PRs that you want to target. (Optional)
* `appSecretName`: A `Secret` name containing a GitHub App secret in [repo-creds format][repo-creds].

[repo-creds]: ../declarative-setup.md#repository-credentials

## GitLab

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ spec:
tokenRef:
secretName: github-token
key: token
# (optional) use a GitHub App to access the API instead of a PAT.
appSecretName: gh-app-repo-creds
template:
# ...
```
Expand All @@ -56,6 +58,9 @@ spec:
* `api`: If using GitHub Enterprise, the URL to access it.
* `allBranches`: By default (false) the template will only be evaluated for the default branch of each repo. If this is true, every branch of every repository will be passed to the filters. If using this flag, you likely want to use a `branchMatch` filter.
* `tokenRef`: A `Secret` name and key containing the GitHub access token to use for requests. If not specified, will make anonymous requests which have a lower rate limit and can only see public repositories.
* `appSecretName`: A `Secret` name containing a GitHub App secret in [repo-creds format][repo-creds].

[repo-creds]: ../declarative-setup.md#repository-credentials

For label filtering, the repository topics are used.

Expand Down
12 changes: 12 additions & 0 deletions manifests/core-install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4527,6 +4527,8 @@ spec:
properties:
api:
type: string
appSecretName:
type: string
labels:
items:
type: string
Expand Down Expand Up @@ -4964,6 +4966,8 @@ spec:
type: boolean
api:
type: string
appSecretName:
type: string
organization:
type: string
tokenRef:
Expand Down Expand Up @@ -6753,6 +6757,8 @@ spec:
properties:
api:
type: string
appSecretName:
type: string
labels:
items:
type: string
Expand Down Expand Up @@ -7190,6 +7196,8 @@ spec:
type: boolean
api:
type: string
appSecretName:
type: string
organization:
type: string
tokenRef:
Expand Down Expand Up @@ -7844,6 +7852,8 @@ spec:
properties:
api:
type: string
appSecretName:
type: string
labels:
items:
type: string
Expand Down Expand Up @@ -8281,6 +8291,8 @@ spec:
type: boolean
api:
type: string
appSecretName:
type: string
organization:
type: string
tokenRef:
Expand Down
Loading

0 comments on commit 506bd3b

Please sign in to comment.