diff --git a/docs/installation.md b/docs/installation.md index cb727a4..a9f722f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -126,6 +126,7 @@ Configuration keys: |`commentArgocdDiffonPR`| Uses ArgoCD API to calculate expected changes to k8s state and comment the resulting "diff" as comment in the PR. Requires ARGOCD_* environment variables, see below. | |`autoMergeNoDiffPRs`| if true, Telefonistka will **merge** promotion PRs that are not expected to change the target clusters. Requires `commentArgocdDiffonPR` and possibly `autoApprovePromotionPrs`(depending on repo branch protection rules)| |`useSHALabelForArgoDicovery`| The default method for discovering relevant ArgoCD applications (for a PR) relies on fetching all applications in the repo and checking the `argocd.argoproj.io/manifest-generate-paths` **annotation**, this might cause a performance issue on a repo with a large number of ArgoCD applications. The alternative is to add SHA1 of the application path as a **label** and rely on ArgoCD server-side filtering, label name is `telefonistka.io/component-path-sha1`.| +|`allowSyncArgoCDAppfromBranchPathRegex`| This controls which component(=ArgoCD apps) are allowed to be "applied" from a PR branch, by setting the ArgoCD application `Target Revision` to PR branch.| Example: @@ -173,6 +174,7 @@ dryRunMode: true autoApprovePromotionPrs: true commentArgocdDiffonPR: true autoMergeNoDiffPRs: true +allowSyncArgoCDAppfromBranchPathRegex: '^workspace/.*$' toggleCommitStatus: override-terrafrom-pipeline: "github-action-terraform" ``` diff --git a/internal/pkg/argocd/argocd.go b/internal/pkg/argocd/argocd.go index dc746f7..8d1b1e9 100644 --- a/internal/pkg/argocd/argocd.go +++ b/internal/pkg/argocd/argocd.go @@ -20,13 +20,18 @@ import ( argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff" "github.com/argoproj/argo-cd/v2/util/argo/normalizers" - argoio "github.com/argoproj/argo-cd/v2/util/io" "github.com/argoproj/gitops-engine/pkg/sync/hook" "github.com/google/go-cmp/cmp" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +type argoCdClients struct { + app application.ApplicationServiceClient + project projectpkg.ProjectServiceClient + setting settings.SettingsServiceClient +} + // DiffElement struct to store diff element details, this represents a single k8s object type DiffElement struct { ObjectGroup string @@ -51,7 +56,7 @@ type DiffResult struct { func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) (foundDiffs bool, diffElements []DiffElement, err error) { liveObjs, err := cmdutil.LiveObjects(resources.Items) if err != nil { - return false, nil, fmt.Errorf("Failed to get live objects: %v", err) + return false, nil, fmt.Errorf("Failed to get live objects: %w", err) } items := make([]objKeyLiveTarget, 0) @@ -59,17 +64,17 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj for _, mfst := range diffOptions.res.Manifests { obj, err := argoappv1.UnmarshalToUnstructured(mfst) if err != nil { - return false, nil, fmt.Errorf("Failed to unmarshal manifest: %v", err) + return false, nil, fmt.Errorf("Failed to unmarshal manifest: %w", err) } unstructureds = append(unstructureds, obj) } groupedObjs, err := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace) if err != nil { - return false, nil, fmt.Errorf("Failed to group objects by key: %v", err) + return false, nil, fmt.Errorf("Failed to group objects by key: %w", err) } items, err = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace) if err != nil { - return false, nil, fmt.Errorf("Failed to group objects for diff: %v", err) + return false, nil, fmt.Errorf("Failed to group objects for diff: %w", err) } for _, item := range items { @@ -91,11 +96,11 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj WithNoCache(). Build() if err != nil { - return false, nil, fmt.Errorf("Failed to build diff config: %v", err) + return false, nil, fmt.Errorf("Failed to build diff config: %w", err) } diffRes, err := argodiff.StateDiff(item.live, item.target, diffConfig) if err != nil { - return false, nil, fmt.Errorf("Failed to diff objects: %v", err) + return false, nil, fmt.Errorf("Failed to diff objects: %w", err) } if diffRes.Modified || item.target == nil || item.live == nil { @@ -111,7 +116,7 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj live = item.live err = json.Unmarshal(diffRes.PredictedLive, target) if err != nil { - return false, nil, fmt.Errorf("Failed to unmarshal predicted live object: %v", err) + return false, nil, fmt.Errorf("Failed to unmarshal predicted live object: %w", err) } } else { live = item.live @@ -123,7 +128,7 @@ func generateArgocdAppDiff(ctx context.Context, app *argoappv1.Application, proj diffElement.Diff, err = diffLiveVsTargetObject(live, target) if err != nil { - return false, nil, fmt.Errorf("Failed to diff live objects: %v", err) + return false, nil, fmt.Errorf("Failed to diff live objects: %w", err) } } diffElements = append(diffElements, diffElement) @@ -144,7 +149,7 @@ func getEnv(key, fallback string) string { return fallback } -func createArgoCdClient() (apiclient.Client, error) { +func createArgoCdClients() (ac argoCdClients, err error) { plaintext, _ := strconv.ParseBool(getEnv("ARGOCD_PLAINTEXT", "false")) insecure, _ := strconv.ParseBool(getEnv("ARGOCD_INSECURE", "false")) @@ -155,11 +160,26 @@ func createArgoCdClient() (apiclient.Client, error) { Insecure: insecure, } - clientset, err := apiclient.NewClient(opts) + client, err := apiclient.NewClient(opts) + if err != nil { + return ac, fmt.Errorf("Error creating ArgoCD API client: %w", err) + } + + _, ac.app, err = client.NewApplicationClient() + if err != nil { + return ac, fmt.Errorf("Error creating ArgoCD app client: %w", err) + } + + _, ac.project, err = client.NewProjectClient() + if err != nil { + return ac, fmt.Errorf("Error creating ArgoCD project client: %w", err) + } + + _, ac.setting, err = client.NewSettingsClient() if err != nil { - return nil, fmt.Errorf("Error creating ArgoCD API client: %v", err) + return ac, fmt.Errorf("Error creating ArgoCD settings client: %w", err) } - return clientset, nil + 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. @@ -178,7 +198,7 @@ func findArgocdAppBySHA1Label(ctx context.Context, componentPath string, repo st } foundApps, err := appClient.List(ctx, &appLabelQuery) if err != nil { - return nil, fmt.Errorf("Error listing ArgoCD applications: %v", err) + return nil, fmt.Errorf("Error listing ArgoCD applications: %w", err) } if len(foundApps.Items) == 0 { return nil, fmt.Errorf("No ArgoCD application found for component path sha1 %s(repo %s), used this label selector: %s", componentPathSha1, repo, labelSelector) @@ -231,6 +251,54 @@ 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 + ac, err := createArgoCdClients() + if err != nil { + return fmt.Errorf("Error creating ArgoCD clients: %w", err) + } + if useSHALabelForArgoDicovery { + foundApp, err = findArgocdAppBySHA1Label(ctx, componentPath, repo, ac.app) + } else { + foundApp, err = findArgocdAppByManifestPathAnnotation(ctx, componentPath, repo, ac.app) + } + if err != nil { + return fmt.Errorf("error finding ArgoCD application for component path %s: %w", componentPath, err) + } + if foundApp.Spec.Source.TargetRevision == revision { + log.Infof("App %s already has revision %s", foundApp.Name, revision) + return nil + } + + patchObject := struct { + Spec struct { + Source struct { + TargetRevision string `json:"targetRevision"` + } `json:"source"` + } `json:"spec"` + }{} + patchObject.Spec.Source.TargetRevision = revision + patchJson, _ := json.Marshal(patchObject) + patch := string(patchJson) + log.Debugf("Patching app %s/%s with: %s", foundApp.Namespace, foundApp.Name, patch) + + patchType := "merge" + _, err = ac.app.Patch(ctx, &application.ApplicationPatchRequest{ + Name: &foundApp.Name, + AppNamespace: &foundApp.Namespace, + PatchType: &patchType, + Patch: &patch, + }) + if err != nil { + return fmt.Errorf("revision patching failed: %w", err) + } else { + log.Infof("ArgoCD App %s revision set to %s", foundApp.Name, revision) + } + + return err +} + 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 @@ -266,6 +334,12 @@ func generateDiffOfAComponent(ctx context.Context, componentPath string, prBranc log.Debugf("Got ArgoCD app %s", app.Name) componentDiffResult.ArgoCdAppName = app.Name componentDiffResult.ArgoCdAppURL = fmt.Sprintf("%s/applications/%s", argoSettings.URL, app.Name) + + if app.Spec.Source.TargetRevision == prBranch { + componentDiffResult.DiffError = fmt.Errorf("App %s already has revision %s as Source Target Revision, skipping diff calculation", app.Name, prBranch) + return componentDiffResult + } + resources, err := appClient.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &app.Name, AppNamespace: &app.Namespace}) if err != nil { componentDiffResult.DiffError = err @@ -313,33 +387,13 @@ func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []st hasComponentDiff = false hasComponentDiffErrors = false // env var should be centralized - client, err := createArgoCdClient() + ac, err := createArgoCdClients() if err != nil { - log.Errorf("Error creating ArgoCD client: %v", err) + log.Errorf("Error creating ArgoCD clients: %v", err) return false, true, nil, err } - conn, appClient, err := client.NewApplicationClient() - if err != nil { - log.Errorf("Error creating ArgoCD app client: %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{}) + argoSettings, err := ac.setting.Get(ctx, &settings.SettingsQuery{}) if err != nil { log.Errorf("Error getting ArgoCD settings: %v", err) return false, true, nil, err @@ -347,7 +401,7 @@ func GenerateDiffOfChangedComponents(ctx context.Context, componentPathList []st log.Debugf("Checking ArgoCD diff for components: %v", componentPathList) for _, componentPath := range componentPathList { - currentDiffResult := generateDiffOfAComponent(ctx, componentPath, prBranch, repo, appClient, projClient, argoSettings, useSHALabelForArgoDicovery) + currentDiffResult := generateDiffOfAComponent(ctx, componentPath, prBranch, repo, ac.app, ac.project, argoSettings, useSHALabelForArgoDicovery) if currentDiffResult.DiffError != nil { log.Errorf("Error generating diff for component %s: %v", componentPath, currentDiffResult.DiffError) hasComponentDiffErrors = true diff --git a/internal/pkg/configuration/config.go b/internal/pkg/configuration/config.go index f13738b..0d2bbc8 100644 --- a/internal/pkg/configuration/config.go +++ b/internal/pkg/configuration/config.go @@ -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) { diff --git a/internal/pkg/githubapi/github.go b/internal/pkg/githubapi/github.go index bf75c41..e49758a 100644 --- a/internal/pkg/githubapi/github.go +++ b/internal/pkg/githubapi/github.go @@ -70,9 +70,9 @@ 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) { 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") @@ -82,7 +82,10 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr } } } +} +func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPrClientDetails, mainGithubClientPair GhClientPair, approverGithubClientPair GhClientPair, ctx context.Context) { + 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 @@ -112,18 +115,10 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr ghPrClientDetails.PrLogger.Errorf("Failed to minimize stale comments: err=%s\n", err) } if config.CommentArgocdDiffonPR { - var componentPathList []string - - // 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) - if DoesPrHasLabel(*eventPayload, "promotion") { - componentPathList = ghPrClientDetails.PrMetadata.PromotedPaths - } else { - componentPathList, err = generateListOfChangedComponentPaths(ghPrClientDetails, config) - if err != nil { - prHandleError = err - ghPrClientDetails.PrLogger.Errorf("Failed to get list of changed components: err=%s\n", err) - } + componentPathList, err := generateListOfChangedComponentPaths(ghPrClientDetails, config) + if err != nil { + prHandleError = err + ghPrClientDetails.PrLogger.Errorf("Failed to get list of changed components: err=%s\n", err) } hasComponentDiff, hasComponentDiffErrors, diffOfChangedComponents, err := argocd.GenerateDiffOfChangedComponents(ctx, componentPathList, ghPrClientDetails.Ref, ghPrClientDetails.RepoURL, config.UseSHALabelForArgoDicovery) @@ -131,7 +126,7 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr prHandleError = err ghPrClientDetails.PrLogger.Errorf("Failed to get ArgoCD diff information: err=%s\n", err) } else { - ghPrClientDetails.PrLogger.Debugf("Successfully got ArgoCD diff\n") + ghPrClientDetails.PrLogger.Debugf("Successfully got ArgoCD diff(comparing live objects against objects rendered form git ref %s)", ghPrClientDetails.Ref) if !hasComponentDiffErrors && !hasComponentDiff { ghPrClientDetails.PrLogger.Debugf("ArgoCD diff is empty, this PR will not change cluster state\n") prLables, resp, err := ghPrClientDetails.GhClientPair.v3Client.Issues.AddLabelsToIssue(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *eventPayload.PullRequest.Number, []string{"noop"}) @@ -154,15 +149,35 @@ 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 { + diffCommentData := struct { + DiffOfChangedComponents []argocd.DiffResult + HasSyncableComponens bool + BranchName string + }{ + DiffOfChangedComponents: diffOfChangedComponents, + BranchName: ghPrClientDetails.Ref, + } + + 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") @@ -289,6 +304,7 @@ func handleEvent(eventPayloadInterface interface{}, mainGhClientCache *lru.Cache PrSHA: *eventPayload.PullRequest.Head.SHA, } + log.Debugf("=== Ref is %s\n", ghPrClientDetails.Ref) HandlePREvent(eventPayload, ghPrClientDetails, mainGithubClientPair, approverGithubClientPair, ctx) case *github.IssueCommentEvent: @@ -301,7 +317,8 @@ func handleEvent(eventPayloadInterface interface{}, mainGhClientCache *lru.Cache "repo": *eventPayload.Repo.Owner.Login + "/" + *eventPayload.Repo.Name, "prNumber": *eventPayload.Issue.Number, }) - if *eventPayload.Comment.User.Login != botIdentity { + // Ignore comment events sent by the bot (this is about who trigger the event not who wrote the comment) + if *eventPayload.Sender.Login != botIdentity { ghPrClientDetails := GhPrClientDetails{ Ctx: ctx, GhClientPair: &mainGithubClientPair, @@ -312,7 +329,7 @@ func handleEvent(eventPayloadInterface interface{}, mainGhClientCache *lru.Cache PrAuthor: *eventPayload.Issue.User.Login, PrLogger: prLogger, } - _ = handleCommentPrEvent(ghPrClientDetails, eventPayload) + _ = handleCommentPrEvent(ghPrClientDetails, eventPayload, botIdentity) } else { log.Debug("Ignoring self comment") } @@ -323,7 +340,38 @@ func handleEvent(eventPayloadInterface interface{}, mainGhClientCache *lru.Cache } } -func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueCommentEvent) error { +func analyzeCommentUpdateCheckBox(newBody string, oldBody string, checkboxIdentifier string) (wasCheckedBefore bool, isCheckedNow bool) { + checkboxPattern := fmt.Sprintf(`(?m)^\s*-\s*\[(.)\]\s*.*$`, checkboxIdentifier) + 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, botIdentity string) error { defaultBranch, _ := ghPrClientDetails.GetDefaultBranch() config, err := GetInRepoConfig(ghPrClientDetails, defaultBranch) if err != nil { @@ -332,6 +380,34 @@ func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueC // Comment events doesn't have Ref/SHA in payload, enriching the object: _, _ = ghPrClientDetails.GetRef() _, _ = ghPrClientDetails.GetSHA() + + // This part should only happen on edits of bot comments on open PRs (I'm not testing Issue vs PR as Telefonsitka only creates PRs at this point) + if *ce.Action == "edited" && *ce.Comment.User.Login == botIdentity && *ce.Issue.State == "open" { + const checkboxIdentifier = "telefonistka-argocd-branch-sync" + checkboxWaschecked, checkboxIsChecked := analyzeCommentUpdateCheckBox(*ce.Comment.Body, *ce.Changes.Body.From, checkboxIdentifier) + if !checkboxWaschecked && checkboxIsChecked { + ghPrClientDetails.PrLogger.Infof("Sync Checkbox was checked") + if config.AllowSyncArgoCDAppfromBranchPathRegex != "" { + ghPrClientDetails.getPrMetadata(ce.Issue.GetBody()) + 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.Ref, ghPrClientDetails.RepoURL, config.UseSHALabelForArgoDicovery) + if err != nil { + ghPrClientDetails.PrLogger.Errorf("Failed to sync ArgoCD app from branch: err=%s\n", err) + } + } + } + } + } + } + + // I should probably deprecated this whole part altogether - it was designed to solve a *very* specific problem that is probably no longer relevant with GitHub Rulesets + // The only reason I'm keeping it is that I don't have a clear feature depreciation policy and if I do remove it should be in a distinct PR for commentSubstring, commitStatusContext := range config.ToggleCommitStatus { if strings.Contains(*ce.Comment.Body, "/"+commentSubstring) { err := ghPrClientDetails.ToggleCommitStatus(commitStatusContext, *ce.Sender.Name) @@ -511,7 +587,24 @@ func handleMergedPrEvent(ghPrClientDetails GhPrClientDetails, prApproverGithubCl } else { commentPlanInPR(ghPrClientDetails, promotions) } - return nil + + if config.AllowSyncArgoCDAppfromBranchPathRegex != "" { + componentPathList, err := generateListOfChangedComponentPaths(ghPrClientDetails, config) + if err != nil { + ghPrClientDetails.PrLogger.Errorf("Failed to get list of changed components for setting ArgoCD app targetRef to HEAD: err=%s\n", err) + } + for _, componentPath := range componentPathList { + if isSyncFromBranchAllowedForThisPath(config.AllowSyncArgoCDAppfromBranchPathRegex, componentPath) { + ghPrClientDetails.PrLogger.Infof("Ensuring ArgoCD app %s is set to HEAD\n", componentPath) + err := argocd.SetArgoCDAppRevision(ghPrClientDetails.Ctx, componentPath, "HEAD", ghPrClientDetails.RepoURL, config.UseSHALabelForArgoDicovery) + if err != nil { + ghPrClientDetails.PrLogger.Errorf("Failed to set ArgoCD app @ %s, to HEAD: err=%s\n", componentPath, err) + } + } + } + } + + return err } // Creating a unique branch name based on the PR number, PR ref and the promotion target paths diff --git a/internal/pkg/githubapi/github_test.go b/internal/pkg/githubapi/github_test.go index 84b5c95..d363487 100644 --- a/internal/pkg/githubapi/github_test.go +++ b/internal/pkg/githubapi/github_test.go @@ -62,3 +62,98 @@ func TestGenerateSafePromotionBranchNameLongTargets(t *testing.T) { t.Errorf("Expected branch name to be less than 250 characters, got %d", len(result)) } } + +func TestAnalyzeCommentUpdateCheckBox(t *testing.T) { + t.Parallel() + tests := map[string]struct { + newBody string + oldBody string + checkboxIdentifier string + expectedWasCheckedBefore bool + expectedIsCheckedNow bool + }{ + "Checkbox is marked": { + oldBody: `This is a comment +foobar +- [ ] Description of checkbox +foobar`, + newBody: `This is a comment +foobar +- [x] Description of checkbox +foobar`, + checkboxIdentifier: "check-slug-1", + expectedWasCheckedBefore: false, + expectedIsCheckedNow: true, + }, + "Checkbox is unmarked": { + oldBody: `This is a comment +foobar +- [x] Description of checkbox +foobar`, + newBody: `This is a comment +foobar +- [ ] Description of checkbox +foobar`, + checkboxIdentifier: "check-slug-1", + expectedWasCheckedBefore: true, + expectedIsCheckedNow: false, + }, + "Checkbox isn't in comment body": { + oldBody: `This is a comment +foobar +foobar`, + newBody: `This is a comment +foobar +foobar`, + checkboxIdentifier: "check-slug-1", + expectedWasCheckedBefore: false, + expectedIsCheckedNow: false, + }, + } + for name, tc := range tests { + tc := tc // capture range variable + name := name + t.Run(name, func(t *testing.T) { + t.Parallel() + wasCheckedBefore, isCheckedNow := analyzeCommentUpdateCheckBox(tc.newBody, tc.oldBody, tc.checkboxIdentifier) + if isCheckedNow != tc.expectedIsCheckedNow { + t.Errorf("%s: Expected isCheckedNow to be %v, got %v", name, tc.expectedIsCheckedNow, isCheckedNow) + } + if wasCheckedBefore != tc.expectedWasCheckedBefore { + t.Errorf("%s: Expected wasCheckedBeforeto to be %v, got %v", name, tc.expectedWasCheckedBefore, wasCheckedBefore) + } + }) + } +} + +func TestIsSyncFromBranchAllowedForThisPath(t *testing.T) { + t.Parallel() + tests := map[string]struct { + allowedPathRegex string + path string + expectedResult bool + }{ + "Path is allowed": { + allowedPathRegex: `^workspace/.*$`, + path: "workspace/app3", + expectedResult: true, + }, + "Path is not allowed": { + allowedPathRegex: `^workspace/.*$`, + path: "clusters/prod/aws/eu-east-1/app3", + expectedResult: false, + }, + } + + for name, tc := range tests { + tc := tc // capture range variable + name := name + t.Run(name, func(t *testing.T) { + t.Parallel() + result := isSyncFromBranchAllowedForThisPath(tc.allowedPathRegex, tc.path) + if result != tc.expectedResult { + t.Errorf("%s: Expected result to be %v, got %v", name, tc.expectedResult, result) + } + }) + } +} diff --git a/internal/pkg/githubapi/promotion.go b/internal/pkg/githubapi/promotion.go index b9b013f..84f1e19 100644 --- a/internal/pkg/githubapi/promotion.go +++ b/internal/pkg/githubapi/promotion.go @@ -170,8 +170,14 @@ type relevantComponent struct { AutoMerge bool } -// This function basically turns the map with struct keys into a list of strings func generateListOfChangedComponentPaths(ghPrClientDetails GhPrClientDetails, config *cfg.Config) (changedComponentPaths []string, err error) { + // If the PR has a list of promoted paths in the PR Telefonistika metadata(=is a promotion PR), we use that + if len(ghPrClientDetails.PrMetadata.PromotedPaths) > 0 { + changedComponentPaths = ghPrClientDetails.PrMetadata.PromotedPaths + return changedComponentPaths, nil + } + + // If not we will use in-repo config to generate it, and turns the map with struct keys into a list of strings relevantComponents, err := generateListOfRelevantComponents(ghPrClientDetails, config) if err != nil { return nil, err diff --git a/templates/argoCD-diff-pr-comment.gotmpl b/templates/argoCD-diff-pr-comment.gotmpl index 7ac719b..c8eacd2 100644 --- a/templates/argoCD-diff-pr-comment.gotmpl +++ b/templates/argoCD-diff-pr-comment.gotmpl @@ -1,6 +1,6 @@ {{define "argoCdDiff"}} Diff of ArgoCD applications: -{{ range $appDiffResult := . }} +{{ range $appDiffResult := .DiffOfChangedComponents }} {{if $appDiffResult.DiffError }} @@ -32,4 +32,12 @@ No diff 🤷 {{- end }} {{- end }} + +{{- if .HasSyncableComponens }} + +- [ ] Set ArgoCD apps Target Revision to `{{ .BranchName }}` + +{{ end}} + + {{- end }}