-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
74d4e98
commit 62d4894
Showing
16 changed files
with
1,578 additions
and
749 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,6 @@ import ( | |
"encoding/json" | ||
goerrors "errors" | ||
"fmt" | ||
"github.com/argoproj/argo-cd/v2/controller/commit" | ||
"math" | ||
"math/rand" | ||
"net/http" | ||
|
@@ -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" | ||
|
@@ -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 | ||
|
@@ -1605,6 +1621,8 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo | |
destinationBranch: destinationBranch, | ||
} | ||
ctrl.hydrationQueue.Add(key) | ||
} else { | ||
logCtx.Debug("No reason to re-hydrate") | ||
} | ||
} | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) | ||
|
||
/** | ||
|
@@ -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 { | ||
|
@@ -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 { | ||
|
@@ -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") { | ||
|
@@ -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 { | ||
|
Oops, something went wrong.