Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion server/neptune/lyft/activities/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ type AtlantisJobEvent struct {
// ProjectName in the atlantis.yaml
RootName string `json:"root_name"`

// Currently we do not track approvers metadata.
ApprovedBy string `json:"approved_by"`
ApprovedTime string `json:"approved_time"`
}
Expand Down Expand Up @@ -83,6 +82,8 @@ type AuditJobRequest struct {
EndTime string
IsForceApply bool
Tags map[string]string
ApprovedBy string
ApprovedTime string
}

func NewAuditActivity(snsTopicArn string) (*Audit, error) {
Expand Down Expand Up @@ -123,6 +124,8 @@ func (a *Audit) AuditJob(ctx context.Context, req AuditJobRequest) error {
Revision: req.Revision,
Project: req.Tags[ProjectTagKey],
Environment: req.Tags[EnvironmentTagKey],
ApprovedBy: req.ApprovedBy,
ApprovedTime: req.ApprovedTime,
}

if req.State == AtlantisJobStateFailure || req.State == AtlantisJobStateSuccess {
Expand Down
10 changes: 8 additions & 2 deletions server/neptune/lyft/notifier/sns.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ package notifier

import (
"context"
"strconv"

"github.com/runatlantis/atlantis/server/neptune/lyft/activities"
t "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/plugins"
"go.temporal.io/sdk/workflow"

"strconv"
)

type auditActivity interface {
Expand Down Expand Up @@ -45,6 +44,11 @@ func (n *SNSNotifier) Notify(ctx workflow.Context, deploymentInfo plugins.Terraf
return nil
}

var approvedTime string
if !jobState.ApprovedTime.IsZero() {
approvedTime = strconv.FormatInt(jobState.ApprovedTime.Unix(), 10)
}

auditJobReq := activities.AuditJobRequest{
Repo: deploymentInfo.Repo,
Root: deploymentInfo.Root,
Expand All @@ -56,6 +60,8 @@ func (n *SNSNotifier) Notify(ctx workflow.Context, deploymentInfo plugins.Terraf
StartTime: startTime,
EndTime: endTime,
IsForceApply: deploymentInfo.Root.TriggerInfo.Type == t.ManualTrigger && deploymentInfo.Root.TriggerInfo.Force,
ApprovedBy: jobState.ApprovedBy,
ApprovedTime: approvedTime,
}

return workflow.ExecuteActivity(ctx, n.Activity.AuditJob, auditJobReq).Get(ctx, nil)
Expand Down
19 changes: 14 additions & 5 deletions server/neptune/lyft/notifier/sns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func testSNSNotifierWorkflow(ctx workflow.Context, r snsNotifierRequest) error {
func TestSNSNotifier_SendsMessage(t *testing.T) {
stTime := time.Now()
endTime := stTime.Add(time.Second * 5)
approvalTime := stTime.Add(-time.Minute)
internalDeploymentInfo := plugins.TerraformDeploymentInfo{
ID: uuid.New(),
Root: terraform.Root{Name: "root"},
Expand All @@ -78,8 +79,10 @@ func TestSNSNotifier_SendsMessage(t *testing.T) {
Status: plugins.SuccessJobStatus,
},
Apply: &plugins.JobState{
Status: plugins.InProgressJobStatus,
StartTime: stTime,
Status: plugins.InProgressJobStatus,
StartTime: stTime,
ApprovedBy: "octocat",
ApprovedTime: approvalTime,
},
},
ExpectedAuditJobRequest: activities.AuditJobRequest{
Expand All @@ -89,6 +92,8 @@ func TestSNSNotifier_SendsMessage(t *testing.T) {
State: activities.AtlantisJobStateRunning,
StartTime: strconv.FormatInt(stTime.Unix(), 10),
IsForceApply: false,
ApprovedBy: "octocat",
ApprovedTime: strconv.FormatInt(approvalTime.Unix(), 10),
},
},
{
Expand Down Expand Up @@ -160,9 +165,11 @@ func TestSNSNotifier_SendsMessage(t *testing.T) {
Status: plugins.SuccessJobStatus,
},
Apply: &plugins.JobState{
Status: plugins.SuccessJobStatus,
StartTime: stTime,
EndTime: endTime,
Status: plugins.SuccessJobStatus,
StartTime: stTime,
EndTime: endTime,
ApprovedBy: "octocat",
ApprovedTime: approvalTime,
},
},
ExpectedAuditJobRequest: activities.AuditJobRequest{
Expand All @@ -173,6 +180,8 @@ func TestSNSNotifier_SendsMessage(t *testing.T) {
StartTime: strconv.FormatInt(stTime.Unix(), 10),
EndTime: strconv.FormatInt(endTime.Unix(), 10),
IsForceApply: false,
ApprovedBy: "octocat",
ApprovedTime: strconv.FormatInt(approvalTime.Unix(), 10),
},
},
}
Expand Down
29 changes: 21 additions & 8 deletions server/neptune/workflows/internal/terraform/gate/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ const (
type PlanStatus int
type PlanReviewSignalRequest struct {
Status PlanStatus

// TODO: Output this info to the checks UI
User string
User string
}

const (
Approved PlanStatus = iota
Rejected
)

// ReviewResult holds the outcome of a plan review gate.
type ReviewResult struct {
Status PlanStatus
ApprovedBy string
ApprovedTime time.Time
}

// Review waits for a plan review signal or a timeout to occur and returns an associated status.
type Review struct {
MetricsHandler client.MetricsHandler
Expand All @@ -40,9 +45,11 @@ type ActionsClient interface {
UpdateApprovalActions(approval terraform.PlanApproval) error
}

func (r *Review) Await(ctx workflow.Context, root terraform.Root, planSummary terraform.PlanSummary) (PlanStatus, error) {
// Await blocks until the plan is approved, rejected, or times out.
// On approval it returns the approver username and the time of approval.
func (r *Review) Await(ctx workflow.Context, root terraform.Root, planSummary terraform.PlanSummary) (ReviewResult, error) {
if root.Plan.Approval.Type == terraform.AutoApproval || planSummary.IsEmpty() {
return Approved, nil
return ReviewResult{Status: Approved}, nil
}

waitStartTime := time.Now()
Expand All @@ -56,8 +63,10 @@ func (r *Review) Await(ctx workflow.Context, root terraform.Root, planSummary te
}

var planReview PlanReviewSignalRequest
var approvedAt time.Time
selector.AddReceive(ch, func(c workflow.ReceiveChannel, more bool) {
ch.Receive(ctx, &planReview)
approvedAt = workflow.Now(ctx)
})

var timedOut bool
Expand All @@ -70,14 +79,18 @@ func (r *Review) Await(ctx workflow.Context, root terraform.Root, planSummary te

err := r.Client.UpdateApprovalActions(root.Plan.Approval)
if err != nil {
return Rejected, errors.Wrap(err, "updating approval actions")
return ReviewResult{Status: Rejected}, errors.Wrap(err, "updating approval actions")
}

selector.Select(ctx)

if timedOut {
return Rejected, nil
return ReviewResult{Status: Rejected}, nil
}

return planReview.Status, nil
return ReviewResult{
Status: planReview.Status,
ApprovedBy: planReview.User,
ApprovedTime: approvedAt,
}, nil
}
27 changes: 13 additions & 14 deletions server/neptune/workflows/internal/terraform/gate/review_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

type res struct {
Status gate.PlanStatus
ReviewResult gate.ReviewResult
ActionsClientCalled bool
ActionsClientCapturedApproval terraform.PlanApproval
}
Expand Down Expand Up @@ -47,14 +47,14 @@ func testReviewWorkflow(ctx workflow.Context, r req) (res, error) {
Client: c,
}

status, err := review.Await(ctx, terraform.Root{
result, err := review.Await(ctx, terraform.Root{
Plan: terraform.PlanJob{
Approval: r.ApprovalOverride,
},
}, r.PlanSummary)

return res{
Status: status,
ReviewResult: result,
ActionsClientCalled: c.called,
ActionsClientCapturedApproval: c.capturedApproval,
}, err
Expand All @@ -80,11 +80,11 @@ func TestAwait_timesOut(t *testing.T) {
err := env.GetWorkflowResult(&r)
assert.NoError(t, err)

assert.Equal(t, r, res{
Status: gate.Rejected,
ActionsClientCalled: true,
ActionsClientCapturedApproval: approvalOverride,
})
assert.Equal(t, gate.Rejected, r.ReviewResult.Status)
assert.Empty(t, r.ReviewResult.ApprovedBy)
assert.True(t, r.ReviewResult.ApprovedTime.IsZero())
assert.True(t, r.ActionsClientCalled)
assert.Equal(t, approvalOverride, r.ActionsClientCapturedApproval)
}

func TestAwait_approvesEmptyPlan(t *testing.T) {
Expand All @@ -101,10 +101,10 @@ func TestAwait_approvesEmptyPlan(t *testing.T) {
err := env.GetWorkflowResult(&r)
assert.NoError(t, err)

assert.Equal(t, r, res{
Status: gate.Approved,
})
assert.Equal(t, gate.Approved, r.ReviewResult.Status)
assert.Empty(t, r.ReviewResult.ApprovedBy)
}

func TestAwait_autoApprove(t *testing.T) {
var suite testsuite.WorkflowTestSuite
env := suite.NewTestWorkflowEnvironment()
Expand All @@ -115,7 +115,6 @@ func TestAwait_autoApprove(t *testing.T) {
err := env.GetWorkflowResult(&r)
assert.NoError(t, err)

assert.Equal(t, r, res{
Status: gate.Approved,
})
assert.Equal(t, gate.Approved, r.ReviewResult.Status)
assert.Empty(t, r.ReviewResult.ApprovedBy)
}
22 changes: 22 additions & 0 deletions server/neptune/workflows/internal/terraform/state/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type UpdateOptions struct {
ValidateSummary conftest.ValidateSummary
StartTime time.Time
EndTime time.Time
ApprovedBy string
ApprovedTime time.Time
}

func NewWorkflowStoreWithGenerator(notifier UpdateNotifier, g urlGenerator, mode terraform.WorkflowMode, id string) *WorkflowStore {
Expand Down Expand Up @@ -165,6 +167,8 @@ func (s *WorkflowStore) UpdateApplyJobWithStatus(status JobStatus, options ...Up
switch status {
case InProgressJobStatus:
s.state.Apply.StartTime = getStartTimeFromOpts(options...)
s.state.Apply.ApprovedBy = getApprovedByFromOpts(options...)
s.state.Apply.ApprovedTime = getApprovedTimeFromOpts(options...)

case FailedJobStatus, SuccessJobStatus:
s.state.Apply.EndTime = getEndTimeFromOpts(options...)
Expand Down Expand Up @@ -200,3 +204,21 @@ func getEndTimeFromOpts(options ...UpdateOptions) time.Time {
}
return time.Time{}
}

func getApprovedByFromOpts(options ...UpdateOptions) string {
for _, o := range options {
if o.ApprovedBy != "" {
return o.ApprovedBy
}
}
return ""
}

func getApprovedTimeFromOpts(options ...UpdateOptions) time.Time {
for _, o := range options {
if !o.ApprovedTime.IsZero() {
return o.ApprovedTime
}
}
return time.Time{}
}
14 changes: 8 additions & 6 deletions server/neptune/workflows/internal/terraform/state/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,18 @@ type Job struct {
Status JobStatus
StartTime time.Time
EndTime time.Time
ApprovedBy string
ApprovedTime time.Time
}

func (j *Job) toExternalJob() *plugins.JobState {
return &plugins.JobState{
ID: j.ID,

// we can probably do this in a cleaner way
Status: plugins.JobStatus(string(j.Status)),
StartTime: j.StartTime,
EndTime: j.EndTime,
ID: j.ID,
Status: plugins.JobStatus(string(j.Status)),
StartTime: j.StartTime,
EndTime: j.EndTime,
ApprovedBy: j.ApprovedBy,
ApprovedTime: j.ApprovedTime,
}
}

Expand Down
8 changes: 5 additions & 3 deletions server/neptune/workflows/internal/terraform/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,21 +241,23 @@ func (r *Runner) Apply(ctx workflow.Context, root *terraform.LocalRoot, serverUR
return errors.Wrap(err, "initializing job")
}

planStatus, err := r.ReviewGate.Await(ctx, root.Root, planResponse.Summary)
reviewResult, err := r.ReviewGate.Await(ctx, root.Root, planResponse.Summary)
if err != nil {
workflow.GetLogger(ctx).Error("error waiting for plan review.", key.ErrKey, err)
return newPlanRejectedError()
}

if planStatus == gate.Rejected {
if reviewResult.Status == gate.Rejected {
if err := r.Store.UpdateApplyJobWithStatus(state.RejectedJobStatus); err != nil {
workflow.GetLogger(ctx).Error("unable to update job with rejected status.", key.ErrKey, err)
}
return newPlanRejectedError()
}

if err := r.Store.UpdateApplyJobWithStatus(state.InProgressJobStatus, state.UpdateOptions{
StartTime: time.Now(),
StartTime: time.Now(),
ApprovedBy: reviewResult.ApprovedBy,
ApprovedTime: reviewResult.ApprovedTime,
}); err != nil {
return newUpdateJobError(err, "unable to update job with success status")
}
Expand Down
10 changes: 6 additions & 4 deletions server/neptune/workflows/plugins/workflow_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ const (

// JobState represents the state of a job at a given time.
type JobState struct {
ID string
Status JobStatus
StartTime time.Time
EndTime time.Time
ID string
Status JobStatus
StartTime time.Time
EndTime time.Time
ApprovedBy string
ApprovedTime time.Time
}

// TerraformWorkflowState contains the state of all jobs in the workflow
Expand Down
Loading