Skip to content

Commit

Permalink
fix: for helm namespace (#23)
Browse files Browse the repository at this point in the history
* feat: manifest hydrator

Signed-off-by: Michael Crenshaw <[email protected]>

* it's monitoring both branches now

Signed-off-by: Michael Crenshaw <[email protected]>

* push works w/ my personal creds

Signed-off-by: Michael Crenshaw <[email protected]>

* write metadata, readme, and commands

Signed-off-by: Michael Crenshaw <[email protected]>

* handle missing branches, missing manifest files, and no-op changes

Signed-off-by: Michael Crenshaw <[email protected]>

* don't set release name or namespace to values from the app CR

Signed-off-by: Michael Crenshaw <[email protected]>

* more determinism

Signed-off-by: Michael Crenshaw <[email protected]>

* handle new branches

Signed-off-by: Michael Crenshaw <[email protected]>

* show hydration progress

Signed-off-by: Michael Crenshaw <[email protected]>

* use workqueue

Signed-off-by: Michael Crenshaw <[email protected]>

* use securejoin, use log contexts, clean up temp dirs

Signed-off-by: Michael Crenshaw <[email protected]>

* use app auth for github only

Signed-off-by: Alexandre Gaudreault <[email protected]>

* it works

Signed-off-by: Alexandre Gaudreault <[email protected]>

* retry failed operations

Signed-off-by: Alexandre Gaudreault <[email protected]>

* fix

Signed-off-by: Alexandre Gaudreault <[email protected]>

* codegen

Signed-off-by: Alexandre Gaudreault <[email protected]>

* it just works

Signed-off-by: Alexandre Gaudreault <[email protected]>

---------

Signed-off-by: Michael Crenshaw <[email protected]>
Signed-off-by: Alexandre Gaudreault <[email protected]>
Co-authored-by: Michael Crenshaw <[email protected]>
  • Loading branch information
agaudreault and crenshaw-dev authored May 23, 2024
1 parent 74d4e98 commit 62d4894
Show file tree
Hide file tree
Showing 16 changed files with 1,578 additions and 749 deletions.
4 changes: 4 additions & 0 deletions assets/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -6425,6 +6425,10 @@
"description": "KubeVersions is the Kubernetes version to use for templating. If not set, defaults to the server's current Kubernetes version.",
"type": "string"
},
"namespace": {
"type": "string",
"title": "ReleaseName is the namespace scope to use. If omitted it will use the application name"
},
"parameters": {
"type": "array",
"title": "Parameters is a list of Helm parameters which are passed to the helm template command upon manifest generation",
Expand Down
2 changes: 2 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ const (
LabelValueSecretTypeRepository = "repository"
// LabelValueSecretTypeRepoCreds indicates a secret type of repository credentials
LabelValueSecretTypeRepoCreds = "repo-creds"
// LabelValueSecretTypeRepoCreds indicates a secret type of repository credentials
LabelValueSecretTypeHydrator = "hydrator"

// AnnotationKeyAppInstance is the Argo CD application name is used as the instance name
AnnotationKeyAppInstance = "argocd.argoproj.io/tracking-id"
Expand Down
61 changes: 39 additions & 22 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
goerrors "errors"
"fmt"
"github.com/argoproj/argo-cd/v2/controller/commit"
"math"
"math/rand"
"net/http"
Expand All @@ -17,6 +16,8 @@ import (
"sync"
"time"

"github.com/argoproj/argo-cd/v2/controller/commit"

clustercache "github.com/argoproj/gitops-engine/pkg/cache"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/health"
Expand Down Expand Up @@ -1586,15 +1587,30 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
logCtx.Errorf("Dry source has not been resolved, skipping")
return
}
if app.Status.SourceHydrator.Revision != revision {
app.Status.SourceHydrator.Revision = revision
app.Status.SourceHydrator.HydrateOperation = &appv1.HydrateOperation{
StartedAt: metav1.Now(),
FinishedAt: nil,
Status: appv1.HydrateOperationPhaseRunning,
if app.Status.SourceHydrator.Revision != revision || (app.Status.SourceHydrator.HydrateOperation != nil && app.Status.SourceHydrator.HydrateOperation.Status != appv1.HydrateOperationPhaseSucceeded) {
restart := false
if app.Status.SourceHydrator.Revision != revision {
restart = true
}
ctrl.persistAppStatus(origApp, &app.Status)
origApp.Status.SourceHydrator = app.Status.SourceHydrator

if app.Status.SourceHydrator.HydrateOperation != nil && app.Status.SourceHydrator.HydrateOperation.Status == appv1.HydrateOperationPhaseFailed {
retryWaitPeriod := 2 * 60 * time.Second
if metav1.Now().Sub(app.Status.SourceHydrator.HydrateOperation.FinishedAt.Time) > retryWaitPeriod {
logCtx.Info("Retrying failed hydration")
restart = true
}
}

if restart {
app.Status.SourceHydrator.HydrateOperation = &appv1.HydrateOperation{
StartedAt: metav1.Now(),
FinishedAt: nil,
Status: appv1.HydrateOperationPhaseRunning,
}
ctrl.persistAppStatus(origApp, &app.Status)
origApp.Status.SourceHydrator = app.Status.SourceHydrator
}

destinationBranch := app.Spec.SourceHydrator.SyncSource.TargetRevision
if app.Spec.SourceHydrator.HydrateTo != nil {
destinationBranch = app.Spec.SourceHydrator.HydrateTo.TargetRevision
Expand All @@ -1605,6 +1621,8 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
destinationBranch: destinationBranch,
}
ctrl.hydrationQueue.Add(key)
} else {
logCtx.Debug("No reason to re-hydrate")
}
}

Expand Down Expand Up @@ -1785,7 +1803,7 @@ func (ctrl *ApplicationController) processHydrationQueueItem() (processNext bool
app.Status.SourceHydrator.HydrateOperation.Status = appv1.HydrateOperationPhaseFailed
failedAt := metav1.Now()
app.Status.SourceHydrator.HydrateOperation.FinishedAt = &failedAt
app.Status.SourceHydrator.HydrateOperation.Message = err.Error()
app.Status.SourceHydrator.HydrateOperation.Message = fmt.Sprintf("Failed to hydrated revision %s: %v", revision, err.Error())
ctrl.persistAppStatus(origApp, &app.Status)
logCtx.Errorf("Failed to hydrate app: %v", err)
return
Expand All @@ -1800,6 +1818,7 @@ func (ctrl *ApplicationController) processHydrationQueueItem() (processNext bool
Status: appv1.HydrateOperationPhaseSucceeded,
Message: "",
}
app.Status.SourceHydrator.Revision = revision
app.Status.SourceHydrator.HydrateOperation = operation
ctrl.persistAppStatus(origApp, &app.Status)
origApp.Status.SourceHydrator = app.Status.SourceHydrator
Expand Down Expand Up @@ -1855,18 +1874,16 @@ func (ctrl *ApplicationController) hydrate(apps []*appv1.Application, refreshTyp
}

manifestsRequest := commit.ManifestsRequest{
RepoURL: repoURL,
SyncBranch: syncBranch,
TargetBranch: targetBranch,
DrySHA: revision,
CommitAuthorName: "Michael Crenshaw",
CommitAuthorEmail: "[email protected]",
CommitMessage: fmt.Sprintf("[Argo CD Bot] hydrate %s", revision),
CommitTime: time.Now(),
Paths: paths,
}

commitService := commit.NewService()
RepoURL: repoURL,
SyncBranch: syncBranch,
TargetBranch: targetBranch,
DrySHA: revision,
CommitMessage: fmt.Sprintf("[Argo CD Bot] hydrate %s", revision),
CommitTime: time.Now(),
Paths: paths,
}

commitService := commit.NewService(ctrl.db)
_, err := commitService.Commit(manifestsRequest)
if err != nil {
return fmt.Errorf("failed to commit hydrated manifests: %w", err)
Expand Down
174 changes: 139 additions & 35 deletions controller/commit/commit.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
package commit

import (
"context"
"encoding/json"
"fmt"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"net/http"
"os"
"os/exec"
"path"
"sigs.k8s.io/yaml"
"strings"
"text/template"
"time"

"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/bradleyfalzon/ghinstallation/v2"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/google/go-github/v35/github"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)

/**
Expand All @@ -25,15 +33,13 @@ type Service interface {
}

type ManifestsRequest struct {
RepoURL string
SyncBranch string
TargetBranch string
DrySHA string
CommitAuthorName string
CommitAuthorEmail string
CommitMessage string
CommitTime time.Time
Paths []PathDetails
RepoURL string
SyncBranch string
TargetBranch string
DrySHA string
CommitMessage string
CommitTime time.Time
Paths []PathDetails
}

type PathDetails struct {
Expand All @@ -54,16 +60,93 @@ type ManifestsResponse struct {
RequestId string
}

func NewService() Service {
return &service{}
func NewService(db db.ArgoDB) Service {
return &service{db: db}
}

type service struct {
db db.ArgoDB
}

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

// Client builds a github client for the given app authentication.
func getAppInstallation(g github_app_auth.Authentication) (*ghinstallation.Transport, 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)
}
return rt, nil
}

func getGitHubInstallationClient(rt *ghinstallation.Transport) *github.Client {
httpClient := http.Client{Transport: rt}
client := github.NewClient(&httpClient)
return client
}

func getGitHubAppClient(g github_app_auth.Authentication) (*github.Client, error) {
var client *github.Client
var err error

// This creates the app authenticated with the bearer JWT, not the installation token.
rt, err := ghinstallation.NewAppsTransport(http.DefaultTransport, g.Id, []byte(g.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to create github app: %w", err)
}

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

}

func (s *service) Commit(r ManifestsRequest) (ManifestsResponse, error) {
var authorName, authorEmail, basicAuth string

ctx := context.TODO()
logCtx := log.WithFields(log.Fields{"repo": r.RepoURL, "branch": r.TargetBranch, "drySHA": r.DrySHA})

repo, err := s.db.GetHydratorCredentials(ctx, r.RepoURL)
if err != nil {
return ManifestsResponse{}, fmt.Errorf("failed to get git credentials: %w", err)
}
if isGitHubApp(repo) {
info := github_app_auth.Authentication{
Id: repo.GithubAppId,
InstallationId: repo.GithubAppInstallationId,
PrivateKey: repo.GithubAppPrivateKey,
}
appInstall, err := getAppInstallation(info)
if err != nil {
return ManifestsResponse{}, err
}
token, err := appInstall.Token(ctx)
if err != nil {
return ManifestsResponse{}, fmt.Errorf("failed to get access token: %w", err)
}
client, err := getGitHubAppClient(info)
if err != nil {
return ManifestsResponse{}, fmt.Errorf("cannot create github client: %w", err)
}
app, _, err := client.Apps.Get(ctx, "")
if err != nil {
return ManifestsResponse{}, fmt.Errorf("cannot get app info: %w", err)
}
appLogin := fmt.Sprintf("%s[bot]", app.GetSlug())
user, _, err := getGitHubInstallationClient(appInstall).Users.Get(ctx, appLogin)
if err != nil {
return ManifestsResponse{}, fmt.Errorf("cannot get app user info: %w", err)
}
authorName = user.GetLogin()
authorEmail = fmt.Sprintf("%d+%[email protected]", user.GetID(), user.GetLogin())
basicAuth = fmt.Sprintf("x-access-token:%s", token)
} else {
logCtx.Warn("No github app credentials were found")
}

logCtx.Debug("Creating temp dir")
dirName, err := uuid.NewRandom()
if err != nil {
Expand All @@ -84,36 +167,57 @@ func (s *service) Commit(r ManifestsRequest) (ManifestsResponse, error) {

// Clone the repo into the temp dir using the git CLI
logCtx.Debugf("Cloning repo %s", r.RepoURL)
err = exec.Command("git", "clone", r.RepoURL, dirPath).Run()
authRepoUrl := r.RepoURL
if basicAuth != "" && strings.HasPrefix(authRepoUrl, "https://github.com/") {
authRepoUrl = fmt.Sprintf("https://%[email protected]/%s", basicAuth, strings.TrimPrefix(authRepoUrl, "https://github.com/"))
}
err = exec.Command("git", "clone", authRepoUrl, dirPath).Run()
if err != nil {
return ManifestsResponse{}, fmt.Errorf("failed to clone repo: %w", err)
}

// Set author name
logCtx.Debugf("Setting author name %s", r.CommitAuthorName)
authorCmd := exec.Command("git", "config", "user.name", r.CommitAuthorName)
authorCmd.Dir = dirPath
out, err := authorCmd.CombinedOutput()
if err != nil {
logCtx.WithError(err).WithField("output", string(out)).Error("failed to set author name")
return ManifestsResponse{}, fmt.Errorf("failed to set author name: %w", err)
if basicAuth != "" {
// This is the dumbest kind of auth and should never make it in main branch
// git config url."https://${TOKEN}@github.com/".insteadOf "https://github.com/"
logCtx.Debugf("Setting auth")
authCmd := exec.Command("git", "config", fmt.Sprintf("url.\"https://%[email protected]/\".insteadOf", basicAuth), "https://github.com/")
authCmd.Dir = dirPath
out, err := authCmd.CombinedOutput()
if err != nil {
logCtx.WithError(err).WithField("output", string(out)).Error("failed to set auth")
return ManifestsResponse{}, fmt.Errorf("failed to set auth: %w", err)
}
}

// Set author email
logCtx.Debugf("Setting author email %s", r.CommitAuthorEmail)
emailCmd := exec.Command("git", "config", "user.email", r.CommitAuthorEmail)
emailCmd.Dir = dirPath
out, err = emailCmd.CombinedOutput()
if err != nil {
logCtx.WithError(err).WithField("output", string(out)).Error("failed to set author email")
return ManifestsResponse{}, fmt.Errorf("failed to set author email: %w", err)
if authorName != "" {
// Set author name
logCtx.Debugf("Setting author name %s", authorName)
authorCmd := exec.Command("git", "config", "user.name", authorName)
authorCmd.Dir = dirPath
out, err := authorCmd.CombinedOutput()
if err != nil {
logCtx.WithError(err).WithField("output", string(out)).Error("failed to set author name")
return ManifestsResponse{}, fmt.Errorf("failed to set author name: %w", err)
}
}

if authorEmail != "" {
// Set author email
logCtx.Debugf("Setting author email %s", authorEmail)
emailCmd := exec.Command("git", "config", "user.email", authorEmail)
emailCmd.Dir = dirPath
out, err := emailCmd.CombinedOutput()
if err != nil {
logCtx.WithError(err).WithField("output", string(out)).Error("failed to set author email")
return ManifestsResponse{}, fmt.Errorf("failed to set author email: %w", err)
}
}

// Checkout the sync branch
logCtx.Debugf("Checking out sync branch %s", r.SyncBranch)
checkoutCmd := exec.Command("git", "checkout", r.SyncBranch)
checkoutCmd.Dir = dirPath
out, err = checkoutCmd.CombinedOutput()
out, err := checkoutCmd.CombinedOutput()
if err != nil {
// If the sync branch doesn't exist, create it as an orphan branch.
if strings.Contains(string(out), "did not match any file(s) known to git") {
Expand Down Expand Up @@ -185,7 +289,7 @@ func (s *service) Commit(r ManifestsRequest) (ManifestsResponse, error) {

// Clear the repo contents using git rm
logCtx.Debug("Clearing repo contents")
rmCmd := exec.Command("git", "rm", "-r", ".")
rmCmd := exec.Command("git", "rm", "-r", "--ignore-unmatch", ".")
rmCmd.Dir = dirPath
out, err = rmCmd.CombinedOutput()
if err != nil {
Expand Down
Loading

0 comments on commit 62d4894

Please sign in to comment.