From de8b41d5197629e89e8f736e17c517f95b0a3275 Mon Sep 17 00:00:00 2001 From: Shawna Monero Date: Wed, 24 Jul 2024 11:41:10 -0700 Subject: [PATCH] add back the stuff that was deleted --- server/legacy/events/policy_filter.go | 54 ++++- server/legacy/events/policy_filter_test.go | 129 ++++++++++++ .../events/pr_project_context_builder.go | 31 ++- server/legacy/events/vcs/github_client.go | 196 +++++++++++++++++- server/legacy/events/vcs/inject.go | 4 +- server/legacy/events/vcs/inject_lyft.go | 2 +- server/legacy/lyft/command/feature_runner.go | 68 +++++- .../lyft/command/feature_runner_test.go | 154 ++++++++++++++ .../neptune/gateway/event/pr_error_handler.go | 25 ++- server/neptune/workflows/activities/github.go | 33 ++- 10 files changed, 667 insertions(+), 29 deletions(-) diff --git a/server/legacy/events/policy_filter.go b/server/legacy/events/policy_filter.go index 0f8bdbde4..0a4a971e6 100644 --- a/server/legacy/events/policy_filter.go +++ b/server/legacy/events/policy_filter.go @@ -60,9 +60,12 @@ func (p *ApprovedPolicyFilter) Filter(ctx context.Context, installationToken int // Dismiss PR reviews when event came from pull request change/atlantis plan comment if trigger == command.AutoTrigger || trigger == command.CommentTrigger { + err := p.dismissStalePRReviews(ctx, installationToken, repo, prNum) + if err != nil { + return failedPolicies, errors.Wrap(err, "failed to dismiss stale PR reviews") + } return failedPolicies, nil } - // Fetch reviewers who approved the PR approvedReviewers, err := p.prReviewFetcher.ListLatestApprovalUsernames(ctx, installationToken, repo, prNum) if err != nil { @@ -82,6 +85,55 @@ func (p *ApprovedPolicyFilter) Filter(ctx context.Context, installationToken int } return filteredFailedPolicies, nil } +func (p *ApprovedPolicyFilter) dismissStalePRReviews(ctx context.Context, installationToken int64, repo models.Repo, prNum int) error { + shouldAllocate, err := p.allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: repo.FullName, + }) + if err != nil { + return errors.Wrap(err, "unable to allocate legacy deprecation feature flag") + } + // if legacy deprecation is enabled, don't dismiss stale PR reviews in legacy workflow + if shouldAllocate { + p.logger.InfoContext(ctx, "legacy deprecation feature flag enabled, not dismissing stale PR reviews") + return nil + } + + approvalReviews, err := p.prReviewFetcher.ListApprovalReviews(ctx, installationToken, repo, prNum) + if err != nil { + return errors.Wrap(err, "failed to fetch GH PR reviews") + } + + for _, approval := range approvalReviews { + isOwner, err := p.approverIsOwner(ctx, installationToken, approval) + if err != nil { + return errors.Wrap(err, "failed to validate approver is owner") + } + if isOwner { + err = p.prReviewDismisser.Dismiss(ctx, installationToken, repo, prNum, approval.GetID()) + if err != nil { + return errors.Wrap(err, "failed to dismiss GH PR reviews") + } + } + } + return nil +} + +func (p *ApprovedPolicyFilter) approverIsOwner(ctx context.Context, installationToken int64, approval *gh.PullRequestReview) (bool, error) { + if approval.GetUser() == nil { + return false, errors.New("failed to identify approver") + } + reviewers := []string{approval.GetUser().GetLogin()} + for _, policy := range p.policies { + isOwner, err := p.reviewersContainsPolicyOwner(ctx, installationToken, reviewers, policy) + if err != nil { + return false, errors.Wrap(err, "validating policy approval") + } + if isOwner { + return true, nil + } + } + return false, nil +} func (p *ApprovedPolicyFilter) reviewersContainsPolicyOwner(ctx context.Context, installationToken int64, reviewers []string, policy valid.PolicySet) (bool, error) { // fetch owners from GH team diff --git a/server/legacy/events/policy_filter_test.go b/server/legacy/events/policy_filter_test.go index 854806cf0..f95f9f155 100644 --- a/server/legacy/events/policy_filter_test.go +++ b/server/legacy/events/policy_filter_test.go @@ -45,6 +45,87 @@ func TestFilter_Approved(t *testing.T) { assert.Empty(t, filteredPolicies) } +func TestFilter_NotApproved(t *testing.T) { + reviewFetcher := &mockReviewFetcher{ + reviews: []*github.PullRequestReview{ + { + User: &github.User{Login: github.String(ownerA)}, + }, + { + User: &github.User{Login: github.String(ownerB)}, + }, + }, + } + teamFetcher := &mockTeamMemberFetcher{ + members: []string{ownerC}, + } + reviewDismisser := &mockReviewDismisser{} + failedPolicies := []valid.PolicySet{ + {Name: policyName, Owner: policyOwner}, + } + + policyFilter := NewApprovedPolicyFilter(reviewFetcher, reviewDismisser, teamFetcher, &testFeatureAllocator{}, failedPolicies, logging.NewNoopCtxLogger(t)) + filteredPolicies, err := policyFilter.Filter(context.Background(), 0, models.Repo{}, 0, command.AutoTrigger, failedPolicies) + assert.NoError(t, err) + assert.False(t, reviewFetcher.listUsernamesIsCalled) + assert.True(t, reviewFetcher.listApprovalsIsCalled) + assert.True(t, teamFetcher.isCalled) + assert.False(t, reviewDismisser.isCalled) + assert.Equal(t, failedPolicies, filteredPolicies) +} + +func TestFilter_DismissalBlockedByFeatureAllocator(t *testing.T) { + reviewFetcher := &mockReviewFetcher{ + reviews: []*github.PullRequestReview{ + { + User: &github.User{Login: github.String(ownerA)}, + }, + }, + } + teamFetcher := &mockTeamMemberFetcher{ + members: []string{ownerA}, + } + reviewDismisser := &mockReviewDismisser{} + failedPolicies := []valid.PolicySet{ + {Name: policyName, Owner: policyOwner}, + } + + policyFilter := NewApprovedPolicyFilter(reviewFetcher, reviewDismisser, teamFetcher, &testFeatureAllocator{Enabled: true}, failedPolicies, logging.NewNoopCtxLogger(t)) + filteredPolicies, err := policyFilter.Filter(context.Background(), 0, models.Repo{}, 0, command.AutoTrigger, failedPolicies) + assert.NoError(t, err) + assert.False(t, reviewFetcher.listUsernamesIsCalled) + assert.False(t, reviewFetcher.listApprovalsIsCalled) + assert.False(t, teamFetcher.isCalled) + assert.False(t, reviewDismisser.isCalled) + assert.Equal(t, failedPolicies, filteredPolicies) +} + +func TestFilter_NotApproved_Dismissal(t *testing.T) { + reviewFetcher := &mockReviewFetcher{ + reviews: []*github.PullRequestReview{ + { + User: &github.User{Login: github.String(ownerA)}, + }, + }, + } + teamFetcher := &mockTeamMemberFetcher{ + members: []string{ownerA}, + } + reviewDismisser := &mockReviewDismisser{} + failedPolicies := []valid.PolicySet{ + {Name: policyName, Owner: policyOwner}, + } + + policyFilter := NewApprovedPolicyFilter(reviewFetcher, reviewDismisser, teamFetcher, &testFeatureAllocator{}, failedPolicies, logging.NewNoopCtxLogger(t)) + filteredPolicies, err := policyFilter.Filter(context.Background(), 0, models.Repo{}, 0, command.AutoTrigger, failedPolicies) + assert.NoError(t, err) + assert.False(t, reviewFetcher.listUsernamesIsCalled) + assert.True(t, reviewFetcher.listApprovalsIsCalled) + assert.True(t, teamFetcher.isCalled) + assert.True(t, reviewDismisser.isCalled) + assert.Equal(t, failedPolicies, filteredPolicies) +} + func TestFilter_NoFailedPolicies(t *testing.T) { reviewFetcher := &mockReviewFetcher{ approvers: []string{ownerB}, @@ -85,6 +166,26 @@ func TestFilter_FailedListLatestApprovalUsernames(t *testing.T) { assert.Equal(t, failedPolicies, filteredPolicies) } +func TestFilter_FailedListApprovalReviews(t *testing.T) { + reviewFetcher := &mockReviewFetcher{ + listApprovalsError: assert.AnError, + } + teamFetcher := &mockTeamMemberFetcher{} + reviewDismisser := &mockReviewDismisser{} + failedPolicies := []valid.PolicySet{ + {Name: policyName, Owner: policyOwner}, + } + + policyFilter := NewApprovedPolicyFilter(reviewFetcher, reviewDismisser, teamFetcher, &testFeatureAllocator{}, failedPolicies, logging.NewNoopCtxLogger(t)) + filteredPolicies, err := policyFilter.Filter(context.Background(), 0, models.Repo{}, 0, command.CommentTrigger, failedPolicies) + assert.Error(t, err) + assert.False(t, reviewFetcher.listUsernamesIsCalled) + assert.True(t, reviewFetcher.listApprovalsIsCalled) + assert.False(t, reviewDismisser.isCalled) + assert.False(t, teamFetcher.isCalled) + assert.Equal(t, failedPolicies, filteredPolicies) +} + func TestFilter_FailedTeamMemberFetch(t *testing.T) { reviewFetcher := &mockReviewFetcher{ approvers: []string{ownerB}, @@ -107,6 +208,34 @@ func TestFilter_FailedTeamMemberFetch(t *testing.T) { assert.Equal(t, failedPolicies, filteredPolicies) } +func TestFilter_FailedDismiss(t *testing.T) { + reviewFetcher := &mockReviewFetcher{ + reviews: []*github.PullRequestReview{ + { + User: &github.User{Login: github.String(ownerB)}, + }, + }, + } + reviewDismisser := &mockReviewDismisser{ + error: assert.AnError, + } + teamFetcher := &mockTeamMemberFetcher{ + members: []string{ownerB}, + } + failedPolicies := []valid.PolicySet{ + {Name: policyName, Owner: policyOwner}, + } + + policyFilter := NewApprovedPolicyFilter(reviewFetcher, reviewDismisser, teamFetcher, &testFeatureAllocator{}, failedPolicies, logging.NewNoopCtxLogger(t)) + filteredPolicies, err := policyFilter.Filter(context.Background(), 0, models.Repo{}, 0, command.AutoTrigger, failedPolicies) + assert.Error(t, err) + assert.False(t, reviewFetcher.listUsernamesIsCalled) + assert.True(t, reviewFetcher.listApprovalsIsCalled) + assert.True(t, teamFetcher.isCalled) + assert.True(t, reviewDismisser.isCalled) + assert.Equal(t, failedPolicies, filteredPolicies) +} + type mockReviewFetcher struct { approvers []string listUsernamesIsCalled bool diff --git a/server/legacy/events/pr_project_context_builder.go b/server/legacy/events/pr_project_context_builder.go index 8b2ad46f8..d339c28d9 100644 --- a/server/legacy/events/pr_project_context_builder.go +++ b/server/legacy/events/pr_project_context_builder.go @@ -1,6 +1,8 @@ package events import ( + "fmt" + "github.com/runatlantis/atlantis/server/config/valid" "github.com/runatlantis/atlantis/server/legacy/events/command" "github.com/runatlantis/atlantis/server/logging" @@ -36,14 +38,23 @@ func (p *PlatformModeProjectContextBuilder) BuildProjectContext( repoDir string, contextFlags *command.ContextFlags, ) []command.ProjectContext { - return buildContext( - ctx, - cmdName, - getSteps(cmdName, prjCfg.PullRequestWorkflow, contextFlags.LogLevel), - p.CommentBuilder, - prjCfg, - commentArgs, - repoDir, - contextFlags, - ) + shouldAllocate, err := p.allocator.ShouldAllocate(feature.PlatformMode, feature.FeatureContext{RepoName: ctx.HeadRepo.FullName}) + if err != nil { + p.Logger.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to allocate for feature: %s, error: %s", feature.PlatformMode, err)) + } + + if shouldAllocate { + return buildContext( + ctx, + cmdName, + getSteps(cmdName, prjCfg.PullRequestWorkflow, contextFlags.LogLevel), + p.CommentBuilder, + prjCfg, + commentArgs, + repoDir, + contextFlags, + ) + } + + return p.delegate.BuildProjectContext(ctx, cmdName, prjCfg, commentArgs, repoDir, contextFlags) } diff --git a/server/legacy/events/vcs/github_client.go b/server/legacy/events/vcs/github_client.go index adc686478..cb86d2808 100644 --- a/server/legacy/events/vcs/github_client.go +++ b/server/legacy/events/vcs/github_client.go @@ -18,6 +18,7 @@ import ( "encoding/base64" "fmt" "net/http" + "strconv" "strings" "time" @@ -34,6 +35,32 @@ import ( "github.com/shurcooL/githubv4" ) +var projectCommandTemplateWithLogs = ` +| **Command Name** | **Project** | **Workspace** | **Status** | **Logs** | +| - | - | - | - | - | +| %s | {%s} | {%s} | {%s} | %s | +` + +var projectCommandTemplate = ` +| **Command Name** | **Project** | **Workspace** | **Status** | +| - | - | - | - | +| %s | {%s} | {%s} | {%s} | +` + +var commandTemplate = ` +| **Command Name** | **Status** | +| - | - | +| %s | {%s} | +:information_source: Visit the checkrun for the root in the navigation panel on your left to view logs and details on the operation. +` + +var commandTemplateWithCount = ` +| **Command Name** | **Num Total** | **Num Success** | **Status** | +| - | - | - | - | +| %s | {%s} | {%s} | {%s} | +:information_source: Visit the checkrun for the root in the navigation panel on your left to view logs and details on the operation. +` + // github checks conclusion type ChecksConclusion int //nolint:golint // avoiding refactor while adding linter action @@ -89,6 +116,8 @@ func (e CheckStatus) String() string { // by GitHub. const ( maxCommentLength = 65536 + // Reference: https://github.com/github/docs/issues/3765 + maxChecksOutputLength = 65535 ) // allows for custom handling of github 404s @@ -464,8 +493,171 @@ func (g *GithubClient) GetRepoStatuses(repo models.Repo, pull models.PullRequest // UpdateStatus updates the status badge on the pull request. // See https://github.com/blog/1227-commit-status-api. func (g *GithubClient) UpdateStatus(ctx context.Context, request types.UpdateStatusRequest) (string, error) { - // since legacy deprecation feature flag was enabled (2024), we don't need to do the updating of check runs - return "", nil + shouldAllocate, err := g.allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: request.Repo.FullName, + }) + if err != nil { + return "", errors.Wrap(err, "unable to allocate legacy deprecation feature flag") + } + // if legacy deprecation is enabled, don't mutate check runs in legacy workflow + if shouldAllocate { + g.logger.InfoContext(ctx, "legacy deprecation feature flag enabled, not updating check runs") + return "", nil + } + + // Empty status ID means we create a new check run + if request.StatusID == "" { + return g.createCheckRun(ctx, request) + } + return request.StatusID, g.updateCheckRun(ctx, request, request.StatusID) +} + +func (g *GithubClient) createCheckRun(ctx context.Context, request types.UpdateStatusRequest) (string, error) { + status, conclusion := g.resolveChecksStatus(request.State) + createCheckRunOpts := github.CreateCheckRunOptions{ + Name: request.StatusName, + HeadSHA: request.Ref, + Status: &status, + Output: g.createCheckRunOutput(request), + } + + if request.DetailsURL != "" { + createCheckRunOpts.DetailsURL = &request.DetailsURL + } + + // Conclusion is required if status is Completed + if status == Completed.String() { + createCheckRunOpts.Conclusion = &conclusion + } + + checkRun, _, err := g.client.Checks.CreateCheckRun(ctx, request.Repo.Owner, request.Repo.Name, createCheckRunOpts) + if err != nil { + return "", err + } + + return strconv.FormatInt(*checkRun.ID, 10), nil +} + +func (g *GithubClient) updateCheckRun(ctx context.Context, request types.UpdateStatusRequest, checkRunID string) error { + status, conclusion := g.resolveChecksStatus(request.State) + updateCheckRunOpts := github.UpdateCheckRunOptions{ + Name: request.StatusName, + Status: &status, + Output: g.createCheckRunOutput(request), + } + + if request.DetailsURL != "" { + updateCheckRunOpts.DetailsURL = &request.DetailsURL + } + + // Conclusion is required if status is Completed + if status == Completed.String() { + updateCheckRunOpts.Conclusion = &conclusion + } + + checkRunIDInt, err := strconv.ParseInt(checkRunID, 10, 64) + if err != nil { + return err + } + + _, _, err = g.client.Checks.UpdateCheckRun(ctx, request.Repo.Owner, request.Repo.Name, checkRunIDInt, updateCheckRunOpts) + return err +} + +func (g *GithubClient) resolveState(state models.VCSStatus) string { + switch state { + case models.QueuedVCSStatus: + return "Queued" + case models.PendingVCSStatus: + return "In Progress" + case models.SuccessVCSStatus: + return "Success" + case models.FailedVCSStatus: + return "Failed" + } + return "Failed" +} + +func (g *GithubClient) createCheckRunOutput(request types.UpdateStatusRequest) *github.CheckRunOutput { + var summary string + + // Project command + if strings.Contains(request.StatusName, ":") { + // plan/apply command + if request.DetailsURL != "" { + summary = fmt.Sprintf(projectCommandTemplateWithLogs, + request.CommandName, + request.Project, + request.Workspace, + g.resolveState(request.State), + fmt.Sprintf("[Logs](%s)", request.DetailsURL), + ) + } else { + summary = fmt.Sprintf(projectCommandTemplate, + request.CommandName, + request.Project, + request.Workspace, + g.resolveState(request.State), + ) + } + } else { + if request.NumSuccess != "" && request.NumTotal != "" { + summary = fmt.Sprintf(commandTemplateWithCount, + request.CommandName, + request.NumTotal, + request.NumSuccess, + g.resolveState(request.State)) + } else { + summary = fmt.Sprintf(commandTemplate, + request.CommandName, + g.resolveState(request.State)) + } + } + + // Add formatting to summary + summary = strings.ReplaceAll(strings.ReplaceAll(summary, "{", "`"), "}", "`") + + checkRunOutput := github.CheckRunOutput{ + Title: &request.StatusName, + Summary: &summary, + } + + if request.Output == "" { + return &checkRunOutput + } + if len(request.Output) > maxChecksOutputLength { + terraformOutputTooLong := "Terraform output is too long for Github UI, please review the above link to view detailed logs." + checkRunOutput.Text = &terraformOutputTooLong + } else { + checkRunOutput.Text = &request.Output + } + return &checkRunOutput +} + +// Github Checks uses Status and Conclusion to report status of the check run. Need to map models.VcsStatus to Status and Conclusion +// Status -> queued, in_progress, completed +// Conclusion -> failure, neutral, cancelled, timed_out, or action_required. (Optional. Required if you provide a status of "completed".) +func (g *GithubClient) resolveChecksStatus(state models.VCSStatus) (string, string) { + status := Queued + conclusion := Neutral + + switch state { + case models.SuccessVCSStatus: + status = Completed + conclusion = Success + + case models.PendingVCSStatus: + status = InProgress + + case models.FailedVCSStatus: + status = Completed + conclusion = Failure + + case models.QueuedVCSStatus: + status = Queued + } + + return status.String(), conclusion.String() } // MarkdownPullLink specifies the string used in a pull request comment to reference another pull request. diff --git a/server/legacy/events/vcs/inject.go b/server/legacy/events/vcs/inject.go index 354a7f5f0..1ac7ec636 100644 --- a/server/legacy/events/vcs/inject.go +++ b/server/legacy/events/vcs/inject.go @@ -3,7 +3,7 @@ package vcs // Declare all package dependencies here func NewPullMergeabilityChecker(vcsStatusPrefix string) MergeabilityChecker { - statusFilters := newValidStatusFilters() + statusFilters := newValidStatusFilters(vcsStatusPrefix) checksFilters := newValidChecksFilters(vcsStatusPrefix) return &PullMergeabilityChecker{ @@ -11,7 +11,7 @@ func NewPullMergeabilityChecker(vcsStatusPrefix string) MergeabilityChecker { } } -func newValidStatusFilters() []ValidStatusFilter { +func newValidStatusFilters(vcsStatusPrefix string) []ValidStatusFilter { return []ValidStatusFilter{ SuccessStateFilter, } diff --git a/server/legacy/events/vcs/inject_lyft.go b/server/legacy/events/vcs/inject_lyft.go index 81e82c663..116160954 100644 --- a/server/legacy/events/vcs/inject_lyft.go +++ b/server/legacy/events/vcs/inject_lyft.go @@ -5,7 +5,7 @@ import "github.com/runatlantis/atlantis/server/legacy/events/vcs/lyft" // Declare all lyft package dependencies here func NewLyftPullMergeabilityChecker(vcsStatusPrefix string) MergeabilityChecker { - statusFilters := newValidStatusFilters() + statusFilters := newValidStatusFilters(vcsStatusPrefix) statusFilters = append(statusFilters, lyft.NewSQFilter()) checksFilters := newValidChecksFilters(vcsStatusPrefix) diff --git a/server/legacy/lyft/command/feature_runner.go b/server/legacy/lyft/command/feature_runner.go index dc45d0bc2..1a0ddae0a 100644 --- a/server/legacy/lyft/command/feature_runner.go +++ b/server/legacy/lyft/command/feature_runner.go @@ -1,6 +1,9 @@ package command import ( + "fmt" + + "github.com/runatlantis/atlantis/server/config/valid" "github.com/runatlantis/atlantis/server/legacy/events" "github.com/runatlantis/atlantis/server/legacy/events/command" "github.com/runatlantis/atlantis/server/logging" @@ -30,9 +33,20 @@ func (a *PlatformModeRunner) Run(ctx *command.Context, cmd *command.Comment) { return } + shouldAllocate, err := a.Allocator.ShouldAllocate(feature.PlatformMode, feature.FeatureContext{RepoName: ctx.HeadRepo.FullName}) + if err != nil { + a.Logger.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to allocate for feature: %s, error: %s", feature.PlatformMode, err)) + } + + // if this isn't allocated don't worry about the rest + if !shouldAllocate { + a.Runner.Run(ctx, cmd) + return + } + // now let's determine whether the repo is configured for platform mode by building commands var projectCmds []command.ProjectContext - projectCmds, err := a.Builder.BuildApplyCommands(ctx, cmd) + projectCmds, err = a.Builder.BuildApplyCommands(ctx, cmd) if err != nil { a.Logger.ErrorContext(ctx.RequestCtx, err.Error()) return @@ -60,26 +74,58 @@ type PlatformModeProjectRunner struct { //create object and test // Plan runs terraform plan for the project described by ctx. func (p *PlatformModeProjectRunner) Plan(ctx command.ProjectContext) command.ProjectResult { - return p.PlatformModeRunner.Plan(ctx) + shouldAllocate, err := p.Allocator.ShouldAllocate(feature.PlatformMode, feature.FeatureContext{RepoName: ctx.HeadRepo.FullName}) + if err != nil { + p.Logger.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to allocate for feature: %s, error: %s", feature.PlatformMode, err)) + } + + if shouldAllocate && (ctx.WorkflowModeType == valid.PlatformWorkflowMode) { + return p.PlatformModeRunner.Plan(ctx) + } + return p.PrModeRunner.Plan(ctx) } // PolicyCheck evaluates policies defined with Rego for the project described by ctx. func (p *PlatformModeProjectRunner) PolicyCheck(ctx command.ProjectContext) command.ProjectResult { - return p.PlatformModeRunner.PolicyCheck(ctx) + shouldAllocate, err := p.Allocator.ShouldAllocate(feature.PlatformMode, feature.FeatureContext{RepoName: ctx.HeadRepo.FullName}) + if err != nil { + p.Logger.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to allocate for feature: %s, error: %s", feature.PlatformMode, err)) + } + + if shouldAllocate && (ctx.WorkflowModeType == valid.PlatformWorkflowMode) { + return p.PlatformModeRunner.PolicyCheck(ctx) + } + return p.PrModeRunner.PolicyCheck(ctx) } // Apply runs terraform apply for the project described by ctx. func (p *PlatformModeProjectRunner) Apply(ctx command.ProjectContext) command.ProjectResult { - return command.ProjectResult{ - Command: command.Apply, - RepoRelDir: ctx.RepoRelDir, - Workspace: ctx.Workspace, - ProjectName: ctx.ProjectName, - StatusID: ctx.StatusID, - ApplySuccess: "atlantis apply is disabled for this project. Please track the deployment when the PR is merged. ", + shouldAllocate, err := p.Allocator.ShouldAllocate(feature.PlatformMode, feature.FeatureContext{RepoName: ctx.HeadRepo.FullName}) + if err != nil { + p.Logger.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to allocate for feature: %s, error: %s", feature.PlatformMode, err)) } + + if shouldAllocate && (ctx.WorkflowModeType == valid.PlatformWorkflowMode) { + return command.ProjectResult{ + Command: command.Apply, + RepoRelDir: ctx.RepoRelDir, + Workspace: ctx.Workspace, + ProjectName: ctx.ProjectName, + StatusID: ctx.StatusID, + ApplySuccess: "atlantis apply is disabled for this project. Please track the deployment when the PR is merged. ", + } + } + return p.PrModeRunner.Apply(ctx) } func (p *PlatformModeProjectRunner) Version(ctx command.ProjectContext) command.ProjectResult { - return p.PlatformModeRunner.Version(ctx) + shouldAllocate, err := p.Allocator.ShouldAllocate(feature.PlatformMode, feature.FeatureContext{RepoName: ctx.HeadRepo.FullName}) + if err != nil { + p.Logger.ErrorContext(ctx.RequestCtx, fmt.Sprintf("unable to allocate for feature: %s, error: %s", feature.PlatformMode, err)) + } + + if shouldAllocate && (ctx.WorkflowModeType == valid.PlatformWorkflowMode) { + return p.PlatformModeRunner.Version(ctx) + } + return p.PrModeRunner.Version(ctx) } diff --git a/server/legacy/lyft/command/feature_runner_test.go b/server/legacy/lyft/command/feature_runner_test.go index 33759d3cc..0aed9e5d5 100644 --- a/server/legacy/lyft/command/feature_runner_test.go +++ b/server/legacy/lyft/command/feature_runner_test.go @@ -353,6 +353,15 @@ func TestPlatformModeProjectRunner_policyCheck(t *testing.T) { }, prModeRunner: &testRunner{}, }, + { + description: "not allocated and platform mode enabled", + shouldAllocate: false, + workflowModeType: valid.PlatformWorkflowMode, + platformRunner: &testRunner{}, + prModeRunner: &testRunner{ + expectedPolicyCheckResult: expectedResult, + }, + }, } for _, c := range cases { @@ -383,3 +392,148 @@ func TestPlatformModeProjectRunner_policyCheck(t *testing.T) { }) } } + +func TestPlatformModeProjectRunner_apply(t *testing.T) { + cases := []struct { + description string + shouldAllocate bool + workflowModeType valid.WorkflowModeType + platformRunner events.ProjectCommandRunner + prModeRunner events.ProjectCommandRunner + subject lyftCommand.PlatformModeProjectRunner + expectedResult command.ProjectResult + }{ + { + description: "allocated and platform mode enabled", + shouldAllocate: true, + workflowModeType: valid.PlatformWorkflowMode, + platformRunner: &testRunner{ + expectedApplyResult: command.ProjectResult{ + RepoRelDir: "reldir", + Workspace: "default", + ProjectName: "project", + StatusID: "id", + Command: command.Apply, + ApplySuccess: "atlantis apply is disabled for this project. Please track the deployment when the PR is merged. ", + }, + }, + expectedResult: command.ProjectResult{ + RepoRelDir: "reldir", + Workspace: "default", + ProjectName: "project", + StatusID: "id", + Command: command.Apply, + ApplySuccess: "atlantis apply is disabled for this project. Please track the deployment when the PR is merged. ", + }, + prModeRunner: &testRunner{}, + }, + { + description: "not allocated and platform mode enabled", + shouldAllocate: false, + workflowModeType: valid.PlatformWorkflowMode, + platformRunner: &testRunner{}, + prModeRunner: &testRunner{ + expectedApplyResult: command.ProjectResult{ + JobID: "1234y", + }, + }, + expectedResult: command.ProjectResult{ + JobID: "1234y", + }, + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + subject := lyftCommand.PlatformModeProjectRunner{ + PlatformModeRunner: c.platformRunner, + PrModeRunner: c.prModeRunner, + Allocator: &testAllocator{ + expectedResult: c.shouldAllocate, + expectedFeatureName: feature.PlatformMode, + expectedCtx: feature.FeatureContext{ + RepoName: "nish/repo", + }, + expectedT: t, + }, + Logger: logging.NewNoopCtxLogger(t), + } + + result := subject.Apply(command.ProjectContext{ + RequestCtx: context.Background(), + HeadRepo: models.Repo{ + FullName: "nish/repo", + }, + RepoRelDir: "reldir", + Workspace: "default", + ProjectName: "project", + StatusID: "id", + WorkflowModeType: c.workflowModeType, + }) + + assert.Equal(t, c.expectedResult, result) + }) + } +} + +func TestPlatformModeProjectRunner_version(t *testing.T) { + expectedResult := command.ProjectResult{ + JobID: "1234y", + } + + cases := []struct { + description string + shouldAllocate bool + workflowModeType valid.WorkflowModeType + platformRunner events.ProjectCommandRunner + prModeRunner events.ProjectCommandRunner + subject lyftCommand.PlatformModeProjectRunner + }{ + { + description: "allocated and platform mode enabled", + shouldAllocate: true, + workflowModeType: valid.PlatformWorkflowMode, + platformRunner: &testRunner{ + expectedVersionResult: expectedResult, + }, + prModeRunner: &testRunner{}, + }, + { + description: "not allocated and platform mode enabled", + shouldAllocate: false, + workflowModeType: valid.PlatformWorkflowMode, + platformRunner: &testRunner{}, + prModeRunner: &testRunner{ + expectedVersionResult: expectedResult, + }, + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + subject := lyftCommand.PlatformModeProjectRunner{ + PlatformModeRunner: c.platformRunner, + PrModeRunner: c.prModeRunner, + Allocator: &testAllocator{ + expectedResult: c.shouldAllocate, + expectedFeatureName: feature.PlatformMode, + expectedCtx: feature.FeatureContext{ + RepoName: "nish/repo", + }, + expectedT: t, + }, + Logger: logging.NewNoopCtxLogger(t), + } + + result := subject.Version(command.ProjectContext{ + RequestCtx: context.Background(), + HeadRepo: models.Repo{ + FullName: "nish/repo", + }, + WorkflowModeType: c.workflowModeType, + }) + + assert.Equal(t, expectedResult, result) + }) + } +} diff --git a/server/neptune/gateway/event/pr_error_handler.go b/server/neptune/gateway/event/pr_error_handler.go index 4345a7d4a..f673cd646 100644 --- a/server/neptune/gateway/event/pr_error_handler.go +++ b/server/neptune/gateway/event/pr_error_handler.go @@ -53,7 +53,19 @@ type LegacyPREventErrorHandler struct { } func (p *LegacyPREventErrorHandler) WrapWithHandling(ctx context.Context, event PREvent, commandName string, executor sync.Executor) sync.Executor { - return executor + allocation, err := p.allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: event.GetRepo().FullName, + }) + + if err != nil { + return p.delegate.WrapWithHandling(ctx, event, commandName, executor) + } + + if allocation { + return executor + } + + return p.delegate.WrapWithHandling(ctx, event, commandName, executor) } type NeptunePREventErrorHandler struct { @@ -62,6 +74,17 @@ type NeptunePREventErrorHandler struct { } func (p *NeptunePREventErrorHandler) WrapWithHandling(ctx context.Context, event PREvent, commandName string, executor sync.Executor) sync.Executor { + allocation, err := p.allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: event.GetRepo().FullName, + }) + + if err != nil { + return p.delegate.WrapWithHandling(ctx, event, commandName, executor) + } + + if !allocation { + return executor + } return p.delegate.WrapWithHandling(ctx, event, commandName, executor) } diff --git a/server/neptune/workflows/activities/github.go b/server/neptune/workflows/activities/github.go index c87b0b8a0..8ec4962d4 100644 --- a/server/neptune/workflows/activities/github.go +++ b/server/neptune/workflows/activities/github.go @@ -95,6 +95,16 @@ type UpdateCheckRunResponse struct { } func (a *githubActivities) GithubUpdateCheckRun(ctx context.Context, request UpdateCheckRunRequest) (UpdateCheckRunResponse, error) { + shouldAllocate, err := a.Allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: request.Repo.GetFullName(), + }) + if err != nil { + activity.GetLogger(ctx).Error("unable to allocate legacy deprecation feature flag", key.ErrKey, err) + } + // skip check run mutation if we're in PR mode and legacy deprecation is not enabled + if request.Mode == terraform.PR && !shouldAllocate { + return UpdateCheckRunResponse{}, nil + } output := github.CheckRunOutput{ Title: &request.Title, Text: &request.Title, @@ -141,6 +151,17 @@ func (a *githubActivities) GithubUpdateCheckRun(ctx context.Context, request Upd } func (a *githubActivities) GithubCreateCheckRun(ctx context.Context, request CreateCheckRunRequest) (CreateCheckRunResponse, error) { + shouldAllocate, err := a.Allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: request.Repo.GetFullName(), + }) + if err != nil { + activity.GetLogger(ctx).Error("unable to allocate legacy deprecation feature flag", key.ErrKey, err) + } + // skip check run mutation if we're in PR mode and legacy deprecation is not enabled + if request.Mode == terraform.PR && !shouldAllocate { + return CreateCheckRunResponse{}, nil + } + output := github.CheckRunOutput{ Title: &request.Title, Text: &request.Title, @@ -390,10 +411,20 @@ type DismissRequest struct { type DismissResponse struct{} func (a *githubActivities) GithubDismiss(ctx context.Context, request DismissRequest) (DismissResponse, error) { + shouldAllocate, err := a.Allocator.ShouldAllocate(feature.LegacyDeprecation, feature.FeatureContext{ + RepoName: request.Repo.GetFullName(), + }) + if err != nil { + return DismissResponse{}, errors.Wrap(err, "unable to allocate legacy deprecation feature flag") + } + // skip PR dismissals if we're in PR mode and legacy deprecation is not enabled + if !shouldAllocate { + return DismissResponse{}, nil + } dismissRequest := &github.PullRequestReviewDismissalRequest{ Message: github.String(request.DismissReason), } - _, _, err := a.Client.DismissReview( + _, _, err = a.Client.DismissReview( ctx, request.Repo.Owner, request.Repo.Name,