mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 00:18:28 +00:00
feat: implement StatusManager architecture for centralized status handling
- Create status package with StatusManager, StatusPolicy, and StatusCleaner - Use Go idiomatic dependency inversion (interfaces where consumed) - Replace scattered silence flag logic with centralized policy decisions - Maintain full backward compatibility with existing silence flags - No new configuration flags needed - Clean separation of concerns and easier testing Architecture benefits: - Single responsibility: all status logic in one package - Policy-driven: centralized silence flag handling - Testable: easy to mock and test status behavior - Maintainable: changes only need to be made in one place - Extensible: easy to add new status types or policies - Clear: explicit status clearing operations available
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/core/config/valid"
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/status"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/gitea"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
@@ -122,9 +123,7 @@ type DefaultCommandRunner struct {
|
||||
// SilenceForkPRErrorsFlag is the name of the flag that controls fork PR's. We use
|
||||
// this in our error message back to the user on a forked PR so they know
|
||||
// how to disable error comment
|
||||
SilenceForkPRErrorsFlag string
|
||||
// SilenceVCSStatusNoProjects is whether to set commit status if no projects are found
|
||||
SilenceVCSStatusNoProjects bool
|
||||
SilenceForkPRErrorsFlag string
|
||||
CommentCommandRunnerByCmd map[command.Name]CommentCommandRunner `validate:"required"`
|
||||
Drainer *Drainer `validate:"required"`
|
||||
PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner `validate:"required"`
|
||||
@@ -133,6 +132,8 @@ type DefaultCommandRunner struct {
|
||||
TeamAllowlistChecker command.TeamAllowlistChecker `validate:"required"`
|
||||
VarFileAllowlistChecker *VarFileAllowlistChecker `validate:"required"`
|
||||
CommitStatusUpdater CommitStatusUpdater `validate:"required"`
|
||||
// StatusManager provides centralized status management
|
||||
StatusManager status.StatusManager `validate:"required"`
|
||||
}
|
||||
|
||||
// RunAutoplanCommand runs plan and policy_checks when a pull request is opened or updated.
|
||||
@@ -205,15 +206,9 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
|
||||
Name: command.Autoplan,
|
||||
}
|
||||
|
||||
// Only set pending status if silence is not enabled
|
||||
// The PlanCommandRunner will handle the final status decision based on project results
|
||||
if !c.SilenceVCSStatusNoProjects {
|
||||
// Update the combined plan commit status to pending
|
||||
if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {
|
||||
ctx.Log.Warn("unable to update plan commit status: %s", err)
|
||||
}
|
||||
} else {
|
||||
ctx.Log.Debug("silence enabled - not setting pending VCS status")
|
||||
// Use StatusManager to handle command start with policy-aware decisions
|
||||
if err := c.StatusManager.HandleCommandStart(ctx, command.Plan); err != nil {
|
||||
ctx.Log.Warn("unable to handle command start status: %s", err)
|
||||
}
|
||||
|
||||
preWorkflowHooksErr := c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd)
|
||||
@@ -374,22 +369,9 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
|
||||
return
|
||||
}
|
||||
|
||||
// Only set pending status if silence is not enabled
|
||||
// The command runners will handle the final status decision based on project results
|
||||
if !c.SilenceVCSStatusNoProjects {
|
||||
// Update the combined plan or apply commit status to pending
|
||||
switch cmd.Name {
|
||||
case command.Plan:
|
||||
if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {
|
||||
ctx.Log.Warn("unable to update plan commit status: %s", err)
|
||||
}
|
||||
case command.Apply:
|
||||
if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil {
|
||||
ctx.Log.Warn("unable to update apply commit status: %s", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.Log.Debug("silence enabled - not setting pending VCS status")
|
||||
// Use StatusManager to handle command start with policy-aware decisions
|
||||
if err := c.StatusManager.HandleCommandStart(ctx, cmd.Name); err != nil {
|
||||
ctx.Log.Warn("unable to handle command start status: %s", err)
|
||||
}
|
||||
|
||||
preWorkflowHooksErr := c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd)
|
||||
|
||||
117
server/events/status/cleaner.go
Normal file
117
server/events/status/cleaner.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate github.com/runatlantis/atlantis/server/events/status --package mocks -o mocks/mock_status_cleaner.go StatusCleaner
|
||||
|
||||
// StatusCleaner provides operations for clearing and managing status checks
|
||||
type StatusCleaner interface {
|
||||
// ClearPendingStatuses clears any pending status checks for the specified commands
|
||||
ClearPendingStatuses(ctx *command.Context, commands []command.Name) error
|
||||
|
||||
// ClearAllStatuses clears all status checks for the PR (plan, apply, policy)
|
||||
ClearAllStatuses(ctx *command.Context) error
|
||||
|
||||
// ClearStatusForCommand clears status for a specific command
|
||||
ClearStatusForCommand(ctx *command.Context, cmdName command.Name) error
|
||||
|
||||
// ResetStatusesToSuccess resets all statuses to success with 0/0 counts
|
||||
ResetStatusesToSuccess(ctx *command.Context) error
|
||||
}
|
||||
|
||||
// DefaultStatusCleaner implements StatusCleaner
|
||||
type DefaultStatusCleaner struct {
|
||||
CommitStatusUpdater CommitStatusUpdater
|
||||
Logger logging.SimpleLogging
|
||||
}
|
||||
|
||||
// NewStatusCleaner creates a new StatusCleaner
|
||||
func NewStatusCleaner(updater CommitStatusUpdater, logger logging.SimpleLogging) StatusCleaner {
|
||||
return &DefaultStatusCleaner{
|
||||
CommitStatusUpdater: updater,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ClearPendingStatuses clears any pending status checks for the specified commands
|
||||
func (c *DefaultStatusCleaner) ClearPendingStatuses(ctx *command.Context, commands []command.Name) error {
|
||||
for _, cmdName := range commands {
|
||||
if err := c.ClearStatusForCommand(ctx, cmdName); err != nil {
|
||||
ctx.Log.Warn("failed to clear pending status for %s: %s", cmdName.String(), err)
|
||||
// Continue clearing other statuses even if one fails
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearAllStatuses clears all status checks for the PR
|
||||
func (c *DefaultStatusCleaner) ClearAllStatuses(ctx *command.Context) error {
|
||||
commands := []command.Name{command.Plan, command.PolicyCheck, command.Apply}
|
||||
return c.ClearPendingStatuses(ctx, commands)
|
||||
}
|
||||
|
||||
// ClearStatusForCommand clears status for a specific command
|
||||
func (c *DefaultStatusCleaner) ClearStatusForCommand(ctx *command.Context, cmdName command.Name) error {
|
||||
ctx.Log.Debug("clearing status for command %s", cmdName.String())
|
||||
// Set success with 0/0 to effectively "clear" the status while keeping it visible
|
||||
return c.CommitStatusUpdater.UpdateCombinedCount(
|
||||
ctx.Log,
|
||||
ctx.Pull.BaseRepo,
|
||||
ctx.Pull,
|
||||
models.SuccessCommitStatus,
|
||||
cmdName,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
// ResetStatusesToSuccess resets all statuses to success with 0/0 counts
|
||||
func (c *DefaultStatusCleaner) ResetStatusesToSuccess(ctx *command.Context) error {
|
||||
ctx.Log.Debug("resetting all statuses to success 0/0")
|
||||
commands := []command.Name{command.Plan, command.PolicyCheck, command.Apply}
|
||||
|
||||
for _, cmdName := range commands {
|
||||
if err := c.CommitStatusUpdater.UpdateCombinedCount(
|
||||
ctx.Log,
|
||||
ctx.Pull.BaseRepo,
|
||||
ctx.Pull,
|
||||
models.SuccessCommitStatus,
|
||||
cmdName,
|
||||
0,
|
||||
0,
|
||||
); err != nil {
|
||||
ctx.Log.Warn("failed to reset status for %s: %s", cmdName.String(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StatusCleanupHelper provides helper functions for common cleanup scenarios
|
||||
type StatusCleanupHelper struct {
|
||||
Cleaner StatusCleaner
|
||||
}
|
||||
|
||||
// NewStatusCleanupHelper creates a new helper
|
||||
func NewStatusCleanupHelper(cleaner StatusCleaner) *StatusCleanupHelper {
|
||||
return &StatusCleanupHelper{
|
||||
Cleaner: cleaner,
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupAfterSilence cleans up any existing statuses when silence flags are enabled
|
||||
func (h *StatusCleanupHelper) CleanupAfterSilence(ctx *command.Context, reason string) error {
|
||||
ctx.Log.Debug("cleaning up statuses due to silence: %s", reason)
|
||||
return h.Cleaner.ClearAllStatuses(ctx)
|
||||
}
|
||||
|
||||
// CleanupPendingOnly clears only pending statuses, leaving success/failure statuses intact
|
||||
func (h *StatusCleanupHelper) CleanupPendingOnly(ctx *command.Context, commands []command.Name) error {
|
||||
ctx.Log.Debug("cleaning up pending statuses only")
|
||||
// This would require querying current status first (future enhancement)
|
||||
// For now, just clear all specified commands
|
||||
return h.Cleaner.ClearPendingStatuses(ctx, commands)
|
||||
}
|
||||
129
server/events/status/manager.go
Normal file
129
server/events/status/manager.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate github.com/runatlantis/atlantis/server/events/status --package mocks -o mocks/mock_status_manager.go StatusManager
|
||||
|
||||
// StatusManager provides high-level status management with policy-aware decisions
|
||||
type StatusManager interface {
|
||||
// High-level operations that handle policy decisions internally
|
||||
HandleCommandStart(ctx *command.Context, cmdName command.Name) error
|
||||
HandleCommandEnd(ctx *command.Context, cmdName command.Name, result *command.Result) error
|
||||
HandleNoProjectsFound(ctx *command.Context, cmdName command.Name) error
|
||||
|
||||
// Direct status operations (bypass policy)
|
||||
SetPending(ctx *command.Context, cmdName command.Name) error
|
||||
SetSuccess(ctx *command.Context, cmdName command.Name, numSuccess, numTotal int) error
|
||||
SetFailure(ctx *command.Context, cmdName command.Name, err error) error
|
||||
|
||||
// Status clearing operations
|
||||
ClearAllStatuses(ctx *command.Context) error
|
||||
ClearStatusForCommand(ctx *command.Context, cmdName command.Name) error
|
||||
|
||||
// Status querying (future enhancement)
|
||||
GetCurrentStatus(repo models.Repo, pull models.PullRequest) (*StatusState, error)
|
||||
}
|
||||
|
||||
// DefaultStatusManager implements StatusManager with policy-aware status decisions
|
||||
type DefaultStatusManager struct {
|
||||
CommitStatusUpdater CommitStatusUpdater
|
||||
Policy StatusPolicy
|
||||
Logger logging.SimpleLogging
|
||||
}
|
||||
|
||||
// NewStatusManager creates a new StatusManager with the given policy and updater
|
||||
func NewStatusManager(updater CommitStatusUpdater, policy StatusPolicy, logger logging.SimpleLogging) StatusManager {
|
||||
return &DefaultStatusManager{
|
||||
CommitStatusUpdater: updater,
|
||||
Policy: policy,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCommandStart handles the start of a command execution
|
||||
func (s *DefaultStatusManager) HandleCommandStart(ctx *command.Context, cmdName command.Name) error {
|
||||
decision := s.Policy.DecideOnStart(ctx, cmdName)
|
||||
return s.executeDecision(ctx, cmdName, decision)
|
||||
}
|
||||
|
||||
// HandleCommandEnd handles the completion of a command execution
|
||||
func (s *DefaultStatusManager) HandleCommandEnd(ctx *command.Context, cmdName command.Name, result *command.Result) error {
|
||||
decision := s.Policy.DecideOnEnd(ctx, cmdName, result)
|
||||
return s.executeDecision(ctx, cmdName, decision)
|
||||
}
|
||||
|
||||
// HandleNoProjectsFound handles the case when no projects are found
|
||||
func (s *DefaultStatusManager) HandleNoProjectsFound(ctx *command.Context, cmdName command.Name) error {
|
||||
decision := s.Policy.DecideOnNoProjects(ctx, cmdName)
|
||||
return s.executeDecision(ctx, cmdName, decision)
|
||||
}
|
||||
|
||||
// SetPending sets a pending status directly (bypasses policy)
|
||||
func (s *DefaultStatusManager) SetPending(ctx *command.Context, cmdName command.Name) error {
|
||||
ctx.Log.Debug("setting pending status for %s", cmdName.String())
|
||||
return s.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, cmdName)
|
||||
}
|
||||
|
||||
// SetSuccess sets a success status directly (bypasses policy)
|
||||
func (s *DefaultStatusManager) SetSuccess(ctx *command.Context, cmdName command.Name, numSuccess, numTotal int) error {
|
||||
ctx.Log.Debug("setting success status for %s (%d/%d)", cmdName.String(), numSuccess, numTotal)
|
||||
return s.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, cmdName, numSuccess, numTotal)
|
||||
}
|
||||
|
||||
// SetFailure sets a failure status directly (bypasses policy)
|
||||
func (s *DefaultStatusManager) SetFailure(ctx *command.Context, cmdName command.Name, err error) error {
|
||||
ctx.Log.Debug("setting failure status for %s: %s", cmdName.String(), err.Error())
|
||||
return s.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, cmdName)
|
||||
}
|
||||
|
||||
// ClearAllStatuses clears all statuses for the PR
|
||||
func (s *DefaultStatusManager) ClearAllStatuses(ctx *command.Context) error {
|
||||
ctx.Log.Debug("clearing all statuses")
|
||||
// Clear each command type individually
|
||||
commands := []command.Name{command.Plan, command.PolicyCheck, command.Apply}
|
||||
for _, cmd := range commands {
|
||||
if err := s.ClearStatusForCommand(ctx, cmd); err != nil {
|
||||
ctx.Log.Warn("failed to clear status for %s: %s", cmd.String(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearStatusForCommand clears status for a specific command
|
||||
func (s *DefaultStatusManager) ClearStatusForCommand(ctx *command.Context, cmdName command.Name) error {
|
||||
ctx.Log.Debug("clearing status for %s", cmdName.String())
|
||||
// Set success with 0/0 to effectively "clear" the status
|
||||
return s.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, cmdName, 0, 0)
|
||||
}
|
||||
|
||||
// GetCurrentStatus returns the current status state (placeholder for future)
|
||||
func (s *DefaultStatusManager) GetCurrentStatus(repo models.Repo, pull models.PullRequest) (*StatusState, error) {
|
||||
// TODO: Implement status querying from VCS
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// executeDecision executes a status decision
|
||||
func (s *DefaultStatusManager) executeDecision(ctx *command.Context, cmdName command.Name, decision StatusDecision) error {
|
||||
switch decision.Operation {
|
||||
case OperationSet:
|
||||
ctx.Log.Debug("status decision: set - %s", decision.Reason)
|
||||
return s.CommitStatusUpdater.UpdateCombinedCount(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, decision.Status, cmdName, decision.NumSuccess, decision.NumTotal)
|
||||
|
||||
case OperationClear:
|
||||
ctx.Log.Debug("status decision: clear - %s", decision.Reason)
|
||||
return s.ClearStatusForCommand(ctx, cmdName)
|
||||
|
||||
case OperationSilence:
|
||||
ctx.Log.Debug("status decision: silence - %s (%s)", decision.Reason, decision.SilenceType)
|
||||
// Do nothing - this is the silence behavior
|
||||
return nil
|
||||
|
||||
default:
|
||||
ctx.Log.Warn("unknown status operation: %d", decision.Operation)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
96
server/events/status/manager_test.go
Normal file
96
server/events/status/manager_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package status_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/status"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
// TestInterfaceCompatibility tests that events.DefaultCommitStatusUpdater
|
||||
// automatically satisfies status.CommitStatusUpdater interface
|
||||
func TestInterfaceCompatibility(t *testing.T) {
|
||||
// Create a real CommitStatusUpdater from events package
|
||||
realUpdater := &events.DefaultCommitStatusUpdater{
|
||||
StatusName: "test-atlantis",
|
||||
}
|
||||
|
||||
// This should compile without any explicit type conversion
|
||||
// because DefaultCommitStatusUpdater automatically satisfies the interface
|
||||
var statusUpdater status.CommitStatusUpdater = realUpdater
|
||||
|
||||
if statusUpdater == nil {
|
||||
t.Fatal("interface compatibility failed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatusManagerCreation tests that we can create a StatusManager with real dependencies
|
||||
func TestStatusManagerCreation(t *testing.T) {
|
||||
// Real dependencies
|
||||
realUpdater := &events.DefaultCommitStatusUpdater{
|
||||
StatusName: "test-atlantis",
|
||||
}
|
||||
|
||||
policy := status.NewSilencePolicy(false, false, false, false)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
|
||||
// Should create successfully - testing the Go implicit interface satisfaction
|
||||
manager := status.NewStatusManager(realUpdater, policy, logger)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("failed to create StatusManager")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSilencePolicyDecisions tests that the policy makes correct decisions
|
||||
func TestSilencePolicyDecisions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
silenceNoProjects bool
|
||||
silenceVCSStatusNoPlans bool
|
||||
silenceVCSStatusNoProjects bool
|
||||
silenceForkPRErrors bool
|
||||
expectedOperation status.StatusOperation
|
||||
}{
|
||||
{
|
||||
name: "no silence flags - should set pending",
|
||||
silenceNoProjects: false,
|
||||
silenceVCSStatusNoPlans: false,
|
||||
silenceVCSStatusNoProjects: false,
|
||||
silenceForkPRErrors: false,
|
||||
expectedOperation: status.OperationSet,
|
||||
},
|
||||
{
|
||||
name: "silence VCS status no projects - should silence",
|
||||
silenceNoProjects: false,
|
||||
silenceVCSStatusNoPlans: false,
|
||||
silenceVCSStatusNoProjects: true,
|
||||
silenceForkPRErrors: false,
|
||||
expectedOperation: status.OperationSilence,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
policy := status.NewSilencePolicy(
|
||||
tt.silenceNoProjects,
|
||||
tt.silenceVCSStatusNoPlans,
|
||||
tt.silenceVCSStatusNoProjects,
|
||||
tt.silenceForkPRErrors,
|
||||
)
|
||||
|
||||
// Create minimal context for testing
|
||||
ctx := &command.Context{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
}
|
||||
|
||||
decision := policy.DecideOnStart(ctx, command.Plan)
|
||||
|
||||
if decision.Operation != tt.expectedOperation {
|
||||
t.Errorf("expected operation %v, got %v", tt.expectedOperation, decision.Operation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
70
server/events/status/models.go
Normal file
70
server/events/status/models.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
// StatusState represents the current state of all status checks for a PR
|
||||
type StatusState struct {
|
||||
PullRequest models.PullRequest
|
||||
Repository models.Repo
|
||||
Statuses map[command.Name]CommandStatus
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// CommandStatus represents the status of a specific command
|
||||
type CommandStatus struct {
|
||||
Command command.Name
|
||||
Status models.CommitStatus
|
||||
NumSuccess int
|
||||
NumTotal int
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// StatusOperation represents an operation to be performed on status
|
||||
type StatusOperation int
|
||||
|
||||
const (
|
||||
// OperationSet sets a status normally
|
||||
OperationSet StatusOperation = iota
|
||||
// OperationClear explicitly clears/removes existing status
|
||||
OperationClear
|
||||
// OperationSilence skips setting status due to silence flags (no VCS interaction)
|
||||
OperationSilence
|
||||
)
|
||||
|
||||
// StatusDecision represents a decision about what status operation to perform
|
||||
type StatusDecision struct {
|
||||
Operation StatusOperation
|
||||
Status models.CommitStatus
|
||||
NumSuccess int
|
||||
NumTotal int
|
||||
Reason string // Human-readable reason for the decision
|
||||
SilenceType string // Which silence flag caused this (for debugging)
|
||||
}
|
||||
|
||||
// SilenceReason contains common silence reasons
|
||||
type SilenceReason struct {
|
||||
NoProjects string
|
||||
NoPlans string
|
||||
ForkPRError string
|
||||
ExplicitSilence string
|
||||
}
|
||||
|
||||
var DefaultSilenceReasons = SilenceReason{
|
||||
NoProjects: "silence enabled and no projects found",
|
||||
NoPlans: "silence enabled and no plans generated",
|
||||
ForkPRError: "fork PR with silence enabled",
|
||||
ExplicitSilence: "explicit silence configuration",
|
||||
}
|
||||
|
||||
// CommitStatusUpdater defines what the status package needs from a commit status updater
|
||||
// Any type that implements these methods will automatically satisfy this interface
|
||||
type CommitStatusUpdater interface {
|
||||
UpdateCombined(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name) error
|
||||
UpdateCombinedCount(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName command.Name, numSuccess int, numTotal int) error
|
||||
}
|
||||
179
server/events/status/policy.go
Normal file
179
server/events/status/policy.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate github.com/runatlantis/atlantis/server/events/status --package mocks -o mocks/mock_status_policy.go StatusPolicy
|
||||
|
||||
// StatusPolicy defines the business logic for when to set, clear, or silence status checks
|
||||
type StatusPolicy interface {
|
||||
// DecideOnStart determines what to do when a command starts
|
||||
DecideOnStart(ctx *command.Context, cmdName command.Name) StatusDecision
|
||||
|
||||
// DecideOnEnd determines what to do when a command completes
|
||||
DecideOnEnd(ctx *command.Context, cmdName command.Name, result *command.Result) StatusDecision
|
||||
|
||||
// DecideOnNoProjects determines what to do when no projects are found
|
||||
DecideOnNoProjects(ctx *command.Context, cmdName command.Name) StatusDecision
|
||||
}
|
||||
|
||||
// SilencePolicy implements StatusPolicy with silence flag awareness
|
||||
type SilencePolicy struct {
|
||||
// Silence flags from user configuration
|
||||
SilenceNoProjects bool
|
||||
SilenceVCSStatusNoPlans bool
|
||||
SilenceVCSStatusNoProjects bool
|
||||
SilenceForkPRErrors bool
|
||||
}
|
||||
|
||||
// NewSilencePolicy creates a new SilencePolicy with the given silence flags
|
||||
func NewSilencePolicy(silenceNoProjects, silenceVCSStatusNoPlans, silenceVCSStatusNoProjects, silenceForkPRErrors bool) StatusPolicy {
|
||||
return &SilencePolicy{
|
||||
SilenceNoProjects: silenceNoProjects,
|
||||
SilenceVCSStatusNoPlans: silenceVCSStatusNoPlans,
|
||||
SilenceVCSStatusNoProjects: silenceVCSStatusNoProjects,
|
||||
SilenceForkPRErrors: silenceForkPRErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// DecideOnStart determines what to do when starting a command
|
||||
func (p *SilencePolicy) DecideOnStart(ctx *command.Context, cmdName command.Name) StatusDecision {
|
||||
// Check fork PR silence
|
||||
if p.shouldSilenceForkPR(ctx) {
|
||||
return StatusDecision{
|
||||
Operation: OperationSilence,
|
||||
Reason: DefaultSilenceReasons.ForkPRError,
|
||||
SilenceType: "SilenceForkPRErrors",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should silence due to VCS status flags
|
||||
if p.SilenceVCSStatusNoProjects {
|
||||
return StatusDecision{
|
||||
Operation: OperationSilence,
|
||||
Reason: "silence VCS status for projects enabled",
|
||||
SilenceType: "SilenceVCSStatusNoProjects",
|
||||
}
|
||||
}
|
||||
|
||||
// Default: set pending status
|
||||
return StatusDecision{
|
||||
Operation: OperationSet,
|
||||
Status: models.PendingCommitStatus,
|
||||
Reason: "command starting - setting pending status",
|
||||
}
|
||||
}
|
||||
|
||||
// DecideOnEnd determines what to do when a command completes
|
||||
func (p *SilencePolicy) DecideOnEnd(ctx *command.Context, cmdName command.Name, result *command.Result) StatusDecision {
|
||||
// Check fork PR silence first
|
||||
if p.shouldSilenceForkPR(ctx) {
|
||||
return StatusDecision{
|
||||
Operation: OperationSilence,
|
||||
Reason: DefaultSilenceReasons.ForkPRError,
|
||||
SilenceType: "SilenceForkPRErrors",
|
||||
}
|
||||
}
|
||||
|
||||
// If command failed, always set failure (unless fork PR silenced)
|
||||
if result.HasErrors() {
|
||||
return StatusDecision{
|
||||
Operation: OperationSet,
|
||||
Status: models.FailedCommitStatus,
|
||||
Reason: "command failed",
|
||||
}
|
||||
}
|
||||
|
||||
// Count successful projects
|
||||
numSuccess := 0
|
||||
numTotal := len(result.ProjectResults)
|
||||
for _, projectResult := range result.ProjectResults {
|
||||
if !projectResult.IsSuccessful() {
|
||||
continue
|
||||
}
|
||||
numSuccess++
|
||||
}
|
||||
|
||||
// If silence is enabled, don't set status
|
||||
if p.SilenceVCSStatusNoProjects {
|
||||
return StatusDecision{
|
||||
Operation: OperationSilence,
|
||||
Reason: "silence VCS status enabled",
|
||||
SilenceType: "SilenceVCSStatusNoProjects",
|
||||
}
|
||||
}
|
||||
|
||||
// Default: set success status with counts
|
||||
return StatusDecision{
|
||||
Operation: OperationSet,
|
||||
Status: models.SuccessCommitStatus,
|
||||
NumSuccess: numSuccess,
|
||||
NumTotal: numTotal,
|
||||
Reason: "command completed successfully",
|
||||
}
|
||||
}
|
||||
|
||||
// DecideOnNoProjects determines what to do when no projects are found
|
||||
func (p *SilencePolicy) DecideOnNoProjects(ctx *command.Context, cmdName command.Name) StatusDecision {
|
||||
// Check fork PR silence first
|
||||
if p.shouldSilenceForkPR(ctx) {
|
||||
return StatusDecision{
|
||||
Operation: OperationSilence,
|
||||
Reason: DefaultSilenceReasons.ForkPRError,
|
||||
SilenceType: "SilenceForkPRErrors",
|
||||
}
|
||||
}
|
||||
|
||||
// Check silence flags specific to no projects/plans
|
||||
if p.shouldSilenceNoProjects(cmdName) {
|
||||
silenceType := p.getSilenceTypeForNoProjects(cmdName)
|
||||
return StatusDecision{
|
||||
Operation: OperationSilence,
|
||||
Reason: DefaultSilenceReasons.NoProjects,
|
||||
SilenceType: silenceType,
|
||||
}
|
||||
}
|
||||
|
||||
// Default: set success status with 0/0
|
||||
return StatusDecision{
|
||||
Operation: OperationSet,
|
||||
Status: models.SuccessCommitStatus,
|
||||
NumSuccess: 0,
|
||||
NumTotal: 0,
|
||||
Reason: "no projects found - setting success 0/0",
|
||||
}
|
||||
}
|
||||
|
||||
// shouldSilenceForkPR checks if this is a fork PR that should be silenced
|
||||
func (p *SilencePolicy) shouldSilenceForkPR(ctx *command.Context) bool {
|
||||
// TODO: Need to determine if this is a fork PR
|
||||
// For now, return false as we don't have access to fork detection logic
|
||||
// This would need to be passed in or determined from context
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldSilenceNoProjects checks if we should silence when no projects are found
|
||||
func (p *SilencePolicy) shouldSilenceNoProjects(cmdName command.Name) bool {
|
||||
// Check different silence flags based on command
|
||||
switch cmdName {
|
||||
case command.Plan:
|
||||
return p.SilenceVCSStatusNoProjects || p.SilenceVCSStatusNoPlans
|
||||
case command.Apply, command.PolicyCheck:
|
||||
return p.SilenceVCSStatusNoProjects
|
||||
default:
|
||||
return p.SilenceVCSStatusNoProjects
|
||||
}
|
||||
}
|
||||
|
||||
// getSilenceTypeForNoProjects returns which silence flag is causing the silence
|
||||
func (p *SilencePolicy) getSilenceTypeForNoProjects(cmdName command.Name) string {
|
||||
if p.SilenceVCSStatusNoProjects {
|
||||
return "SilenceVCSStatusNoProjects"
|
||||
}
|
||||
if cmdName == command.Plan && p.SilenceVCSStatusNoPlans {
|
||||
return "SilenceVCSStatusNoPlans"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
@@ -63,6 +63,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
"github.com/runatlantis/atlantis/server/events/command"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/status"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver"
|
||||
@@ -898,7 +899,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
AllowForkPRsFlag: config.AllowForkPRsFlag,
|
||||
SilenceForkPRErrors: userConfig.SilenceForkPRErrors,
|
||||
SilenceForkPRErrorsFlag: config.SilenceForkPRErrorsFlag,
|
||||
SilenceVCSStatusNoProjects: userConfig.SilenceVCSStatusNoProjects,
|
||||
DisableAutoplan: userConfig.DisableAutoplan,
|
||||
DisableAutoplanLabel: userConfig.DisableAutoplanLabel,
|
||||
Drainer: drainer,
|
||||
@@ -909,6 +909,16 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
VarFileAllowlistChecker: varFileAllowlistChecker,
|
||||
CommitStatusUpdater: commitStatusUpdater,
|
||||
}
|
||||
|
||||
// Create StatusManager with silence policy
|
||||
statusPolicy := status.NewSilencePolicy(
|
||||
userConfig.SilenceNoProjects,
|
||||
userConfig.SilenceVCSStatusNoPlans,
|
||||
userConfig.SilenceVCSStatusNoProjects,
|
||||
userConfig.SilenceForkPRErrors,
|
||||
)
|
||||
statusManager := status.NewStatusManager(commitStatusUpdater, statusPolicy, logger)
|
||||
commandRunner.StatusManager = statusManager
|
||||
repoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user