Skip to content

Commit

Permalink
Allow setting argoCD revision to PR git SHA
Browse files Browse the repository at this point in the history
  • Loading branch information
Oded-B committed Jun 28, 2024
1 parent e2b552d commit 1a0b2dc
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 47 deletions.
80 changes: 54 additions & 26 deletions internal/pkg/argocd/argocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -144,7 +145,9 @@ func getEnv(key, fallback string) string {
return fallback
}

func createArgoCdClient() (apiclient.Client, error) {
func createArgoCdClients() (appClient application.ApplicationServiceClient, projClient projectpkg.ProjectServiceClient, settingClient settings.SettingsServiceClient, err error) {
var conn io.Closer

plaintext, _ := strconv.ParseBool(getEnv("ARGOCD_PLAINTEXT", "false"))
insecure, _ := strconv.ParseBool(getEnv("ARGOCD_INSECURE", "false"))

Expand All @@ -155,11 +158,27 @@ func createArgoCdClient() (apiclient.Client, error) {
Insecure: insecure,
}

clientset, err := apiclient.NewClient(opts)
client, err := apiclient.NewClient(opts)
if err != nil {
return nil, nil, nil, fmt.Errorf("Error creating ArgoCD API client: %v", err)
}

conn, appClient, err = client.NewApplicationClient()

Check failure on line 166 in internal/pkg/argocd/argocd.go

View workflow job for this annotation

GitHub Actions / golangci-lint

ineffectual assignment to conn (ineffassign)
if err != nil {
return nil, fmt.Errorf("Error creating ArgoCD API client: %v", err)
return nil, nil, nil, fmt.Errorf("Error creating ArgoCD app client: %v", err)
}
return clientset, nil

conn, projClient, err = client.NewProjectClient()

Check failure on line 171 in internal/pkg/argocd/argocd.go

View workflow job for this annotation

GitHub Actions / golangci-lint

ineffectual assignment to conn (ineffassign)
if err != nil {
return nil, nil, nil, fmt.Errorf("Error creating ArgoCD project client: %v", err)
}

conn, settingClient, err = client.NewSettingsClient()
if err != nil {
return nil, nil, nil, fmt.Errorf("Error creating ArgoCD settings client: %v", err)
}
defer argoio.Close(conn)
return
}

// findArgocdAppBySHA1Label finds an ArgoCD application by the SHA1 label of the component path it's supposed to avoid performance issues with the "manifest-generate-paths" annotation method which requires pulling all ArgoCD applications(!) on every PR event.
Expand Down Expand Up @@ -231,6 +250,35 @@ func findArgocdAppByManifestPathAnnotation(ctx context.Context, componentPath st
return nil, fmt.Errorf("No ArgoCD application found with manifest-generate-paths annotation that matches %s(looked at repo %s, checked %v apps) ", componentPath, repo, len(allRepoApps.Items))
}

func SetArgoCDAppRevision(ctx context.Context, componentPath string, revision string, repo string, useSHALabelForArgoDicovery bool) error {
var foundApp *argoappv1.Application
var err error
appClient, _, _, err := createArgoCdClients()

Check failure on line 256 in internal/pkg/argocd/argocd.go

View workflow job for this annotation

GitHub Actions / golangci-lint

ineffectual assignment to err (ineffassign)
if useSHALabelForArgoDicovery {
foundApp, err = findArgocdAppBySHA1Label(ctx, componentPath, repo, appClient)

Check failure on line 258 in internal/pkg/argocd/argocd.go

View workflow job for this annotation

GitHub Actions / golangci-lint

ineffectual assignment to err (ineffassign)
} else {
foundApp, err = findArgocdAppByManifestPathAnnotation(ctx, componentPath, repo, appClient)
}
if foundApp.Spec.Source.TargetRevision == revision {
log.Infof("App %s already has revision %s", foundApp.Name, revision)
return nil
}

foundApp.Spec.Source.TargetRevision = revision

_, err = appClient.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
Name: &foundApp.Name,
Spec: &foundApp.Spec,
AppNamespace: &foundApp.Namespace,
})

if err != nil {
return fmt.Errorf("Error setting app %s revision to %s failed: %v", foundApp.Name, revision, err)
}

return nil
}

func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranch string, repo string, appClient application.ApplicationServiceClient, projClient projectpkg.ProjectServiceClient, argoSettings *settings.Settings, useSHALabelForArgoDicovery bool) (componentDiffResult DiffResult) {
componentDiffResult.ComponentPath = componentPath

Expand Down Expand Up @@ -313,32 +361,12 @@ func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []st
hasComponentDiff = false
hasComponentDiffErrors = false
// env var should be centralized
client, err := createArgoCdClient()
if err != nil {
log.Errorf("Error creating ArgoCD client: %v", err)
return false, true, nil, err
}

conn, appClient, err := client.NewApplicationClient()
appClient, projClient, settingClient, err := createArgoCdClients()
if err != nil {
log.Errorf("Error creating ArgoCD app client: %v", err)
log.Errorf("Error creating ArgoCD clients: %v", err)
return false, true, nil, err
}
defer argoio.Close(conn)

conn, projClient, err := client.NewProjectClient()
if err != nil {
log.Errorf("Error creating ArgoCD project client: %v", err)
return false, true, nil, err
}
defer argoio.Close(conn)

conn, settingClient, err := client.NewSettingsClient()
if err != nil {
log.Errorf("Error creating ArgoCD settings client: %v", err)
return false, true, nil, err
}
defer argoio.Close(conn)
argoSettings, err := settingClient.Get(ctx, &settings.SettingsQuery{})
if err != nil {
log.Errorf("Error getting ArgoCD settings: %v", err)
Expand Down
19 changes: 10 additions & 9 deletions internal/pkg/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,16 @@ type Config struct {
PromotionPaths []PromotionPath `yaml:"promotionPaths"`

// Generic configuration
PromtionPrLables []string `yaml:"promtionPRlables"`
DryRunMode bool `yaml:"dryRunMode"`
AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"`
ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"`
WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"`
WhProxtSkipTLSVerifyUpstream bool `yaml:"whProxtSkipTLSVerifyUpstream"`
CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"`
AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"`
UseSHALabelForArgoDicovery bool `yaml:"useSHALabelForArgoDicovery"`
PromtionPrLables []string `yaml:"promtionPRlables"`
DryRunMode bool `yaml:"dryRunMode"`
AutoApprovePromotionPrs bool `yaml:"autoApprovePromotionPrs"`
ToggleCommitStatus map[string]string `yaml:"toggleCommitStatus"`
WebhookEndpointRegexs []WebhookEndpointRegex `yaml:"webhookEndpointRegexs"`
WhProxtSkipTLSVerifyUpstream bool `yaml:"whProxtSkipTLSVerifyUpstream"`
CommentArgocdDiffonPR bool `yaml:"commentArgocdDiffonPR"`
AutoMergeNoDiffPRs bool `yaml:"autoMergeNoDiffPRs"`
AllowSyncArgoCDAppfromBranchPathRegex string `yaml:"allowSyncArgoCDAppfromBranchPathRegex"`
UseSHALabelForArgoDicovery bool `yaml:"useSHALabelForArgoDicovery"`
}

func ParseConfigFromYaml(y string) (*Config, error) {
Expand Down
110 changes: 99 additions & 11 deletions internal/pkg/githubapi/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ func (pm prMetadata) serialize() (string, error) {
return base64.StdEncoding.EncodeToString(pmJson), nil
}

func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPrClientDetails, mainGithubClientPair GhClientPair, approverGithubClientPair GhClientPair, ctx context.Context) {
func (ghPrClientDetails *GhPrClientDetails) getPrMetadata(prBody string) {

Check failure on line 73 in internal/pkg/githubapi/github.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary leading newline (whitespace)

prMetadataRegex := regexp.MustCompile(`<!--\|.*\|(.*)\|-->`)
serializedPrMetadata := prMetadataRegex.FindStringSubmatch(eventPayload.PullRequest.GetBody())
serializedPrMetadata := prMetadataRegex.FindStringSubmatch(prBody)
if len(serializedPrMetadata) == 2 {
if serializedPrMetadata[1] != "" {
ghPrClientDetails.PrLogger.Info("Found PR metadata")
Expand All @@ -83,6 +84,11 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr
}
}

}

Check failure on line 87 in internal/pkg/githubapi/github.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary trailing newline (whitespace)

func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPrClientDetails, mainGithubClientPair GhClientPair, approverGithubClientPair GhClientPair, ctx context.Context) {

Check failure on line 89 in internal/pkg/githubapi/github.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary leading newline (whitespace)

ghPrClientDetails.getPrMetadata(eventPayload.PullRequest.GetBody())
// wasCommitStatusSet and the placement of SetCommitStatus in the flow is used to ensure an API call is only made where it needed
wasCommitStatusSet := false

Expand Down Expand Up @@ -154,15 +160,34 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr
}
}

err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "argoCdDiff", "argoCD-diff-pr-comment.gotmpl", diffOfChangedComponents)
if err != nil {
prHandleError = err
log.Errorf("Failed to generate ArgoCD diff comment template: err=%s\n", err)
}
err = commentPR(ghPrClientDetails, templateOutput)
if err != nil {
prHandleError = err
log.Errorf("Failed to comment ArgoCD diff: err=%s\n", err)
if len(diffOfChangedComponents) > 0 {

Check failure on line 163 in internal/pkg/githubapi/github.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary leading newline (whitespace)

diffCommentData := struct {
diffOfChangedComponents []argocd.DiffResult
hasSyncableComponens bool
}{
diffOfChangedComponents: diffOfChangedComponents,
}

for _, componentPath := range componentPathList {
if isSyncFromBranchAllowedForThisPath(config.AllowSyncArgoCDAppfromBranchPathRegex, componentPath) {
diffCommentData.hasSyncableComponens = true
break
}
}

err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "argoCdDiff", "argoCD-diff-pr-comment.gotmpl", diffCommentData)
if err != nil {
prHandleError = err
log.Errorf("Failed to generate ArgoCD diff comment template: err=%s\n", err)
}
err = commentPR(ghPrClientDetails, templateOutput)
if err != nil {
prHandleError = err
log.Errorf("Failed to comment ArgoCD diff: err=%s\n", err)
}
} else {
ghPrClientDetails.PrLogger.Debugf("Diff not find affected ArogCD apps")
}
}
ghPrClientDetails.PrLogger.Infoln("Checking for Drift")
Expand Down Expand Up @@ -323,6 +348,36 @@ func handleEvent(eventPayloadInterface interface{}, mainGhClientCache *lru.Cache
}
}

func analyzeCommentUpdateCheckBox(newBody string, oldBody string, checkboxPattern string) (wasCheckedBefore bool, isCheckedNow bool) {
checkBoxRegex := regexp.MustCompile(checkboxPattern)
oldCheckBoxContent := checkBoxRegex.FindStringSubmatch(oldBody)
newCheckBoxContent := checkBoxRegex.FindStringSubmatch(newBody)

// I'm grabbing the second group of the regex, which is the checkbox content (either "x" or " ")
// The first element of the result is the whole match
if len(newCheckBoxContent) < 2 || len(oldCheckBoxContent) < 2 {
return false, false
}
if len(newCheckBoxContent) >= 2 {
if newCheckBoxContent[1] == "x" {
isCheckedNow = true
}
}

if len(oldCheckBoxContent) >= 2 {
if oldCheckBoxContent[1] == "x" {
wasCheckedBefore = true
}
}

return
}

func isSyncFromBranchAllowedForThisPath(allowedPathRegex string, path string) bool {
allowedPathsRegex := regexp.MustCompile(allowedPathRegex)
return allowedPathsRegex.MatchString(path)
}

func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueCommentEvent) error {
defaultBranch, _ := ghPrClientDetails.GetDefaultBranch()
config, err := GetInRepoConfig(ghPrClientDetails, defaultBranch)
Expand All @@ -332,6 +387,39 @@ func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueC
// Comment events doesn't have Ref/SHA in payload, enriching the object:
_, _ = ghPrClientDetails.GetRef()
_, _ = ghPrClientDetails.GetSHA()

checkboxPattern := `(?m)^\s*-\s*\[(.)\]\s*<!-- telefonistka-argocd-branch-sync -->.*$`
checkboxWaschecked, checkboxIsChecked := analyzeCommentUpdateCheckBox(*ce.Comment.Body, *ce.Issue.Body, checkboxPattern)
if !checkboxWaschecked && checkboxIsChecked {
ghPrClientDetails.PrLogger.Infof("Sync Checkbox was checked")
if config.AllowSyncArgoCDAppfromBranchPathRegex != "" {
ghPrClientDetails.getPrMetadata(ce.Issue.GetBody()) // TODO is issue is not a PR but an actual issue, this will fail?

// Promotion PR have the list of paths to promote in the PR metadata
// For non promotion PR, we will generate the list of changed components based the PR changed files and the telefonistka configuration(sourcePath)
var componentPathList []string
if len(ghPrClientDetails.PrMetadata.PromotedPaths) > 0 {
componentPathList = ghPrClientDetails.PrMetadata.PromotedPaths
} else {
componentPathList, err = generateListOfChangedComponentPaths(ghPrClientDetails, config)
if err != nil {
ghPrClientDetails.PrLogger.Errorf("Failed to get list of changed components: err=%s\n", err)
}
}

for _, componentPath := range componentPathList {
if isSyncFromBranchAllowedForThisPath(config.AllowSyncArgoCDAppfromBranchPathRegex, componentPath) {
err := argocd.SetArgoCDAppRevision(ghPrClientDetails.Ctx, componentPath, ghPrClientDetails.PrSHA, ghPrClientDetails.RepoURL, config.UseSHALabelForArgoDicovery)
if err != nil {
ghPrClientDetails.PrLogger.Errorf("Failed to sync ArgoCD app from branch: err=%s\n", err)
}
}

}

Check failure on line 418 in internal/pkg/githubapi/github.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary trailing newline (whitespace)
}
}

// I should probably deprecated this in part altogether.
for commentSubstring, commitStatusContext := range config.ToggleCommitStatus {
if strings.Contains(*ce.Comment.Body, "/"+commentSubstring) {
err := ghPrClientDetails.ToggleCommitStatus(commitStatusContext, *ce.Sender.Name)
Expand Down
87 changes: 87 additions & 0 deletions internal/pkg/githubapi/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,90 @@ func TestGenerateSafePromotionBranchNameLongTargets(t *testing.T) {
t.Errorf("Expected branch name to be less than 250 characters, got %d", len(result))
}
}

// Testing a case when a checkbox is marked
func TestAnalyzeCommentUpdateCheckBoxChecked(t *testing.T) {
t.Parallel()
newBody := `This is a comment
foobar
- [x] <!-- check-slug-1 --> Description of checkbox
foobar`

oldBody := `This is a comment
foobar
- [ ] <!-- check-slug-1 --> Description of checkbox
foobar`
checkboxPattern := `(?m)^\s*-\s*\[(.)\]\s*<!-- check-slug-1 -->.*$`

wasCheckedBefore, isCheckedNow := analyzeCommentUpdateCheckBox(newBody, oldBody, checkboxPattern)
if !isCheckedNow {
t.Error("Expected isCheckedNow to be true")
}
if wasCheckedBefore {
t.Errorf("Expected wasCheckedBeforeto be false, actaully got %t", wasCheckedBefore)

Check failure on line 85 in internal/pkg/githubapi/github_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`actaully` is a misspelling of `actually` (misspell)
}
}

// Testing a case when a checkbox is unmarked
func TestAnalyzeCommentUpdateCheckBoxUnChecked(t *testing.T) {
t.Parallel()
newBody := `This is a comment
foobar
- [ ] <!-- check-slug-1 --> Description of checkbox
foobar`

oldBody := `This is a comment
foobar
- [x] <!-- check-slug-1 --> Description of checkbox
foobar`
checkboxPattern := `(?m)^\s*-\s*\[(.)\]\s*<!-- check-slug-1 -->.*$`

wasCheckedBefore, isCheckedNow := analyzeCommentUpdateCheckBox(newBody, oldBody, checkboxPattern)
if isCheckedNow {
t.Error("Expected isCheckedNow to be false")
}
if !wasCheckedBefore {
t.Error("Expected wasCheckedBeforeto be true")
}
}

// Testing a case when a checkbox isn't in the comment body
func TestAnalyzeCommentUpdateCheckBoxNonRelevent(t *testing.T) {
t.Parallel()
newBody := `This is a comment
foobar
foobar`

oldBody := `This is a comment
foobar2
foobar2`
checkboxPattern := `(?m)^\s*-\s*\[(.)\]\s*<!-- check-slug-1 -->.*$`

wasCheckedBefore, isCheckedNow := analyzeCommentUpdateCheckBox(newBody, oldBody, checkboxPattern)
if isCheckedNow {
t.Error("Expected isCheckedNow to be false")
}
if wasCheckedBefore {
t.Error("Expected wasCheckedBeforeto be false")
}
}

func TestIsSyncFromBranchAllowedForThisPathTrue(t *testing.T) {
t.Parallel()
allowedPathRegex := `^workspace/.*$`
path := "workspace/app3"
result := isSyncFromBranchAllowedForThisPath(allowedPathRegex, path)
if !result {
t.Error("Expected result to be true")
}
}

func TestIsSyncFromBranchAllowedForThisPathFalse(t *testing.T) {
t.Parallel()
allowedPathRegex := `^workspace/.*$`
path := "clusters/prod/aws/eu-east-1/app3"
result := isSyncFromBranchAllowedForThisPath(allowedPathRegex, path)
if result {
t.Error("Expected result to be false")
}
}
Loading

0 comments on commit 1a0b2dc

Please sign in to comment.