Skip to content

Commit

Permalink
use shared auth, better error handling
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
  • Loading branch information
crenshaw-dev committed May 29, 2024
1 parent f1c58bb commit e598333
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 38 deletions.
1 change: 1 addition & 0 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
&ctrl.appStateManager,
ctrl.settingsMgr,
ctrl.getAppProj,
ctrl.db,
).Run()

<-ctx.Done()
Expand Down
161 changes: 123 additions & 38 deletions controller/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ package controller
import (
"context"
"fmt"
"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/errors"
logutils "github.com/argoproj/argo-cd/v2/util/log"
"github.com/argoproj/argo-cd/v2/util/settings"
"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v62/github"
"github.com/google/shlex"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"net/http"
"net/url"
"os"
"os/exec"
Expand All @@ -24,32 +29,34 @@ import (
"time"
)

const ArgoCDGitHubUsername = "crenshaw-dev"
const PreviewSleepDuration = 60 * time.Second
const ArgoCDGitHubUsername = "gitops-promoter[bot]"
const PreviewSleepDuration = 10 * time.Second

type Previewer struct {
appLister *applisters.ApplicationLister
appStateManager *AppStateManager
settingsManager *settings.SettingsManager
getAppProject func(app *v1alpha1.Application) (*v1alpha1.AppProject, error)
ghClient *github.Client
ghContext context.Context
appLabelKey string
diffConfig argodiff.DiffConfig
db db.ArgoDB
}

func NewPreviewer(
appLister *applisters.ApplicationLister,
appStateManager *AppStateManager,
settingsManager *settings.SettingsManager,
getAppProject func(app *v1alpha1.Application) (*v1alpha1.AppProject, error),
db db.ArgoDB,
) (p *Previewer) {
p = &Previewer{}
p.appLister = appLister
p.appStateManager = appStateManager
p.settingsManager = settingsManager
p.getAppProject = getAppProject
p.ghClient = github.NewClient(nil).WithAuthToken(os.Getenv("GITHUB_TOKEN"))
p.db = db

p.ghContext = context.Background()
appLabelKey, err := p.settingsManager.GetAppInstanceLabelKey()
errors.CheckError(err)
Expand All @@ -70,14 +77,27 @@ func NewPreviewer(
// Run is the main loop for the preview controller
func (p *Previewer) Run() {
for {
repoMap, err := p.getRepoMap()
if err != nil {
log.Errorf("failed to get repo map: %v", err)
}
// Poll for new PR/PR Commit on listened to repos to dry manifest branch
for repoURL, apps := range p.getRepoMap() {
owner, repo := p.getOwnerRepo(repoURL)
for repoURL, apps := range repoMap {
owner, repoName := p.getOwnerRepo(repoURL)

ghClient, err := p.getClient(repoURL)
if err != nil {
log.Errorf("failed to get GitHub client: %v", err)
continue
}

baseRevision := apps[0].Spec.SourceHydrator.DrySource.TargetRevision
if baseRevision == "HEAD" {
repo, _, err := p.ghClient.Repositories.Get(p.ghContext, owner, repo)
errors.CheckError(err)
repo, _, err := ghClient.Repositories.Get(p.ghContext, owner, repoName)
if err != nil {
log.Errorf("failed to get repo %s/%s: %v", owner, repoName, err)
continue
}
baseRevision = repo.GetDefaultBranch()
}

Expand All @@ -86,39 +106,91 @@ func (p *Previewer) Run() {
Base: baseRevision,
}

pullRequests, _, err := p.ghClient.PullRequests.List(p.ghContext, owner, repo, opts)
errors.CheckError(err)
pullRequests, _, err := ghClient.PullRequests.List(p.ghContext, owner, repoName, opts)
if err != nil {
log.Errorf("failed to get PRs: %v", err)
continue
}
for _, pr := range pullRequests {
comment, found := p.getComment(owner, repo, pr)
comment, err := p.getComment(owner, repoName, pr)
if err != nil {
log.Errorf("failed to get comment: %v", err)
continue
}
commentBody, err := p.makeComment(apps, pr.Base.GetRef(), pr.Head.GetRef())
if err != nil {
log.Errorf("failed to make comment: %v", err)
continue
}
newComment := &github.IssueComment{
// pr.Base is PR Target (branch that will receive changes)
// pr.Head is PR Source (changes we want to integrate)
Body: github.String(p.makeComment(apps, pr.Base.GetRef(), pr.Head.GetRef())),
Body: github.String(commentBody),
}
if found {
if comment != nil {
if comment.GetBody() == newComment.GetBody() {
continue
}
_, _, err := p.ghClient.Issues.EditComment(p.ghContext, owner, repo, comment.GetID(), newComment)
errors.CheckError(err)
_, _, err := ghClient.Issues.EditComment(p.ghContext, owner, repoName, comment.GetID(), newComment)
if err != nil {
log.Errorf("failed to edit comment %d: %v", comment.GetID(), err)
}
} else {
// 4. create
_, _, err := p.ghClient.Issues.CreateComment(p.ghContext, owner, repo, pr.GetNumber() /* PR Issue ID */, newComment)
errors.CheckError(err)
_, _, err := ghClient.Issues.CreateComment(p.ghContext, owner, repoName, pr.GetNumber(), newComment)
if err != nil {
log.Errorf("failed to create comment: %v", err)
}
}
}
}
time.Sleep(PreviewSleepDuration)
}
}

func (p *Previewer) getRepoMap() map[string][]*v1alpha1.Application {
func (p *Previewer) getClient(repoURL string) (*github.Client, error) {
repo, err := p.db.GetHydratorCredentials(context.Background(), repoURL)
if err != nil {
return nil, fmt.Errorf("failed to get repo %s: %w", repoURL, err)
}
if !isGitHubApp(repo) {
panic("Only GitHub App credentials are supported")
}
info := github_app_auth.Authentication{
Id: repo.GithubAppId,
InstallationId: repo.GithubAppInstallationId,
PrivateKey: repo.GithubAppPrivateKey,
}
client, err := getGitHubAppClient(info)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub App client: %w", err)
}
return client, nil
}

func isGitHubApp(cred *v1alpha1.Repository) bool {
return cred.GithubAppPrivateKey != "" && cred.GithubAppId != 0 && cred.GithubAppInstallationId != 0
}

func getGitHubAppClient(g github_app_auth.Authentication) (*github.Client, error) {
// This creates the app authenticated with the bearer JWT, not the installation token.
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 transport: %w", err)
}

httpClient := http.Client{Transport: rt}
client := github.NewClient(&httpClient)
return client, nil
}

func (p *Previewer) getRepoMap() (map[string][]*v1alpha1.Application, error) {
// Get list of unique Repos from all Applications
var repoMap = map[string][]*v1alpha1.Application{}

apps, err := (*p.appLister).List(labels.Everything())
if err != nil {
panic(fmt.Errorf("error while fetching the apps list: %w", err))
return nil, fmt.Errorf("failed to list applications: %w", err)
}
for i := 0; i < len(apps); i++ {
if apps[i].Spec.SourceHydrator == nil {
Expand All @@ -132,7 +204,7 @@ func (p *Previewer) getRepoMap() map[string][]*v1alpha1.Application {
}
repoMap[repoURL] = append(repoMap[repoURL], app)
}
return repoMap
return repoMap, nil
}

func (p *Previewer) getOwnerRepo(repoUrl string) (string, string) {
Expand All @@ -147,25 +219,32 @@ func (p *Previewer) getOwnerRepo(repoUrl string) (string, string) {
return parts[1], parts[2]
}

func (p *Previewer) getComment(owner string, repo string, pr *github.PullRequest) (*github.IssueComment, bool) {
prComments, resp, err := p.ghClient.Issues.ListComments(p.ghContext, owner, repo, pr.GetNumber(), nil)
errors.CheckError(err)
func (p *Previewer) getComment(owner string, repo string, pr *github.PullRequest) (*github.IssueComment, error) {
ghClient, err := p.getClient(fmt.Sprintf("https://github.com/%s/%s", owner, repo))
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
}

prComments, resp, err := ghClient.Issues.ListComments(p.ghContext, owner, repo, pr.GetNumber(), nil)
if err != nil {
return nil, fmt.Errorf("failed to get PR comments: %w", err)
}

if resp.StatusCode != 200 {
panic(fmt.Errorf("failed to get PR comments: %d", resp.StatusCode))
return nil, fmt.Errorf("failed to get PR comments: %d", resp.StatusCode)
}

for i := 0; i < len(prComments); i++ {
if prComments[i].GetUser().GetLogin() == ArgoCDGitHubUsername {
return prComments[i], true
return prComments[i], nil
}
}
return nil, false
return nil, nil
}

func (p *Previewer) makeComment(apps []*v1alpha1.Application, baseBranch string, headBranch string) (commentBody string) {
func (p *Previewer) makeComment(apps []*v1alpha1.Application, baseBranch string, headBranch string) (string, error) {

commentBody = fmt.Sprintf("\n## From branch %s to branch %s\n", headBranch, baseBranch)
commentBody := fmt.Sprintf("\n## From branch %s to branch %s\n", headBranch, baseBranch)

// Sort the apps by name.
// This is to ensure that the diff is consistent across runs.
Expand All @@ -177,50 +256,56 @@ func (p *Previewer) makeComment(apps []*v1alpha1.Application, baseBranch string,
// Produce diff
app := apps[i]
project, err := p.getAppProject(app)
errors.CheckError(err)
if err != nil {
return "", fmt.Errorf("failed to get app project: %w", err)
}

baseUnstructured, err := p.getBranchManifest(app, project, baseBranch)
errors.CheckError(err)
if err != nil {
return "", fmt.Errorf("failed to get base branch manifest: %w", err)
}

headUnstructured, err := p.getBranchManifest(app, project, headBranch)
errors.CheckError(err)
if err != nil {
return "", fmt.Errorf("failed to get head branch manifest: %w", err)
}

commentBody += fmt.Sprintf("\n### for target application %s\n", app.Name)

tempDir, err := os.MkdirTemp("", "argocd-diff")
if err != nil {
panic(err)
return "", fmt.Errorf("failed to create temp dir: %w", err)
}
targetFile := path.Join(tempDir, "target.yaml")
targetData := []byte("")
if baseUnstructured != nil {
targetData, err = yaml.Marshal(baseUnstructured)
if err != nil {
panic(err)
return "", fmt.Errorf("failed to marshal base unstructured: %w", err)
}
}
err = os.WriteFile(targetFile, targetData, 0644)
if err != nil {
panic(err)
return "", fmt.Errorf("failed to write target file: %w", err)
}
liveFile := path.Join(tempDir, "base.yaml")
liveData := []byte("")
if headUnstructured != nil {
liveData, err = yaml.Marshal(headUnstructured)
if err != nil {
panic(err)
return "", fmt.Errorf("failed to marshal head unstructured: %w", err)
}
}
err = os.WriteFile(liveFile, liveData, 0644)
if err != nil {
panic(err)
return "", fmt.Errorf("failed to write live file: %w", err)
}
cmdBinary := "diff"
var args []string
if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" {
parts, err := shlex.Split(envDiff)
if err != nil {
panic(err)
return "", fmt.Errorf("failed to split env diff: %w", err)
}
cmdBinary = parts[0]
args = parts[1:]
Expand All @@ -230,7 +315,7 @@ func (p *Previewer) makeComment(apps []*v1alpha1.Application, baseBranch string,

commentBody += "```diff\n" + string(out) + "```\n"
}
return commentBody
return commentBody, nil
}

// Get Hydrated Branch's manifest.yaml
Expand Down

0 comments on commit e598333

Please sign in to comment.