From 48d277baa33998b78a7b207216fb1b6408ddc2f9 Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Thu, 10 Nov 2022 14:42:07 +1100 Subject: [PATCH 1/4] Add OPA support for task stages: - Added a new API for task stage override --- helper_test.go | 30 ++++ mocks/task_stages_mocks.go | 15 ++ policy_evaluation_beta_test.go | 26 +-- policy_integration_beta_test.go | 6 +- run.go | 1 + task_result.go | 1 + task_stages.go | 65 ++++++- task_stages_integration_beta_test.go | 260 +++++++++++++++++++++++++++ 8 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 task_stages_integration_beta_test.go diff --git a/helper_test.go b/helper_test.go index c8436db5b..7fed70d16 100644 --- a/helper_test.go +++ b/helper_test.go @@ -556,6 +556,36 @@ func createPolicySet(t *testing.T, client *Client, org *Organization, policies [ } } +func createPolicySetWithOptions(t *testing.T, client *Client, org *Organization, policies []*Policy, workspaces []*Workspace, opts PolicySetCreateOptions) (*PolicySet, func()) { + var orgCleanup func() + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + ctx := context.Background() + ps, err := client.PolicySets.Create(ctx, org.Name, PolicySetCreateOptions{ + Name: String(randomString(t)), + Policies: policies, + Workspaces: workspaces, + Kind: opts.Kind, + Overridable: opts.Overridable, + }) + if err != nil { + t.Fatal(err) + } + return ps, func() { + if err := client.PolicySets.Delete(ctx, ps.ID); err != nil { + t.Errorf("Error destroying policy set! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "PolicySet: %s\nError: %s", ps.ID, err) + } + if orgCleanup != nil { + orgCleanup() + } + } +} + func createPolicySetVersion(t *testing.T, client *Client, ps *PolicySet) (*PolicySetVersion, func()) { var psCleanup func() diff --git a/mocks/task_stages_mocks.go b/mocks/task_stages_mocks.go index 99ee51438..47d470aa9 100644 --- a/mocks/task_stages_mocks.go +++ b/mocks/task_stages_mocks.go @@ -50,6 +50,21 @@ func (mr *MockTaskStagesMockRecorder) List(ctx, runID, options interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTaskStages)(nil).List), ctx, runID, options) } +// Override mocks base method. +func (m *MockTaskStages) Override(ctx context.Context, taskStageID string) (*tfe.TaskStage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Override", ctx, taskStageID) + ret0, _ := ret[0].(*tfe.TaskStage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Override indicates an expected call of Override. +func (mr *MockTaskStagesMockRecorder) Override(ctx, taskStageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Override", reflect.TypeOf((*MockTaskStages)(nil).Override), ctx, taskStageID) +} + // Read mocks base method. func (m *MockTaskStages) Read(ctx context.Context, taskStageID string, options *tfe.TaskStageReadOptions) (*tfe.TaskStage, error) { m.ctrl.T.Helper() diff --git a/policy_evaluation_beta_test.go b/policy_evaluation_beta_test.go index 3603e8649..8358029b7 100644 --- a/policy_evaluation_beta_test.go +++ b/policy_evaluation_beta_test.go @@ -47,7 +47,7 @@ func TestPolicyEvaluationList_Beta(t *testing.T) { require.NotEmpty(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) - assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluation)) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) polEvaluation, err := client.PolicyEvaluations.List(ctx, taskStageList.Items[0].ID, nil) require.NoError(t, err) @@ -104,10 +104,10 @@ func TestPolicySetOutcomeList_Beta(t *testing.T) { require.NotEmpty(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) - assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluation)) - assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID)) - polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, nil) require.NoError(t, err) @@ -124,10 +124,10 @@ func TestPolicySetOutcomeList_Beta(t *testing.T) { require.NotEmpty(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) - assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluation)) - assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID)) - polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID opts := &PolicySetOutcomeListOptions{ Filter: map[string]PolicySetOutcomeListFilter{ @@ -153,10 +153,10 @@ func TestPolicySetOutcomeList_Beta(t *testing.T) { require.NotEmpty(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) - assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluation)) - assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID)) - polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID opts := &PolicySetOutcomeListOptions{ Filter: map[string]PolicySetOutcomeListFilter{ @@ -216,10 +216,10 @@ func TestPolicySetOutcomeRead_Beta(t *testing.T) { require.NotEmpty(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) - assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluation)) - assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluations[0].ID)) - polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + polEvaluationID := taskStageList.Items[0].PolicyEvaluations[0].ID polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, nil) require.NoError(t, err) diff --git a/policy_integration_beta_test.go b/policy_integration_beta_test.go index 02b3eb881..ee8ad3b7e 100644 --- a/policy_integration_beta_test.go +++ b/policy_integration_beta_test.go @@ -235,10 +235,9 @@ func TestPoliciesList_Beta(t *testing.T) { defer pTestCleanup2() opaOptions := PolicyCreateOptions{ Kind: OPA, - Query: String("terraform.policy1.deny"), + Query: String("data.example.rule"), Enforce: []*EnforcementOptions{ { - Path: String(".rego"), Mode: EnforcementMode(EnforcementMandatory), }, }, @@ -322,10 +321,9 @@ func TestPoliciesUpdate_Beta(t *testing.T) { options := PolicyCreateOptions{ Description: String("A sample policy"), Kind: OPA, - Query: String("terraform.main"), + Query: String("data.example.rule"), Enforce: []*EnforcementOptions{ { - Path: String(".rego"), Mode: EnforcementMode(EnforcementMandatory), }, }, diff --git a/run.go b/run.go index 5b72e0a68..6655e2b23 100644 --- a/run.go +++ b/run.go @@ -78,6 +78,7 @@ const ( RunPrePlanCompleted RunStatus = "pre_plan_completed" RunPrePlanRunning RunStatus = "pre_plan_running" RunQueuing RunStatus = "queuing" + RunAwaitingDecision RunStatus = "post_plan_awaiting_decision" ) // RunSource represents a source type of a run. diff --git a/task_result.go b/task_result.go index 21ebccc3e..e6b53d8d5 100644 --- a/task_result.go +++ b/task_result.go @@ -29,6 +29,7 @@ const ( TaskPending TaskResultStatus = "pending" TaskRunning TaskResultStatus = "running" TaskUnreachable TaskResultStatus = "unreachable" + TaskErrored TaskResultStatus = "errored" ) // TaskEnforcementLevel is an enum that describes the enforcement levels for a run task diff --git a/task_stages.go b/task_stages.go index 059436717..8d72eb05f 100644 --- a/task_stages.go +++ b/task_stages.go @@ -15,8 +15,12 @@ type TaskStages interface { // Read a task stage by ID Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) - // List all task stages for a given rrun + // List all task stages for a given run List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error) + + // **Note: This function is still in BETA and subject to change.** + // Override a task stage for a given run + Override(ctx context.Context, taskStageID string) (*TaskStage, error) } // taskStages implements TaskStages @@ -33,17 +37,43 @@ const ( PreApply Stage = "pre_apply" ) +// TaskStageStatus is an enum that represents all possible statuses for a task stage +type TaskStageStatus string + +const ( + TaskStagePending TaskStageStatus = "pending" + TaskStageRunning TaskStageStatus = "running" + TaskStagePassed TaskStageStatus = "passed" + TaskStageFailed TaskStageStatus = "failed" + TaskStageAwaitingOverride TaskStageStatus = "awaiting_override" +) + +// Permissions represents the permission types for overridding a task stage +type Permissions struct { + CanOverridePolicy *bool `jsonapi:"attr,can-override-policy"` + CanOverrideTasks *bool `jsonapi:"attr,can-override-tasks"` + CanOverride *bool `jsonapi:"attr,can-override"` +} + +// Actions represents a task stage actions +type Actions struct { + IsOverridable *bool `jsonapi:"attr, overridable"` +} + // TaskStage represents a TFC/E run's stage where run tasks can occur type TaskStage struct { ID string `jsonapi:"primary,task-stages"` Stage Stage `jsonapi:"attr,stage"` + Status TaskStageStatus `jsonapi:"attr,status"` StatusTimestamps TaskStageStatusTimestamps `jsonapi:"attr,status-timestamps"` CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + Permissions *Permissions `jsonapi:"attr,permissions"` + Actions *Actions `jsonapi:"attr,actions"` - Run *Run `jsonapi:"relation,run"` - TaskResults []*TaskResult `jsonapi:"relation,task-results"` - PolicyEvaluation []*PolicyEvaluation `jsonapi:"relation,policy-evaluations"` + Run *Run `jsonapi:"relation,run"` + TaskResults []*TaskResult `jsonapi:"relation,task-results"` + PolicyEvaluations []*PolicyEvaluation `jsonapi:"relation,policy-evaluations"` } // TaskStageList represents a list of task stages @@ -66,6 +96,9 @@ type TaskStageIncludeOpt string const TaskStageTaskResults TaskStageIncludeOpt = "task_results" +// **Note: This field is still in BETA and subject to change.** +const PolicyEvaluationsTaskResults TaskStageIncludeOpt = "policy_evaluations" + // TaskStageReadOptions represents the set of options when reading a task stage type TaskStageReadOptions struct { // Optional: A list of relations to include. @@ -123,6 +156,28 @@ func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageL return tlist, nil } +// **Note: This function is still in BETA and subject to change.** +// Override a task stages for a run +func (s *taskStages) Override(ctx context.Context, taskStageID string) (*TaskStage, error) { + if !validStringID(&taskStageID) { + return nil, ErrInvalidTaskStageID + } + + u := fmt.Sprintf("task-stages/%s/actions/override", taskStageID) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + + t := &TaskStage{} + err = req.Do(ctx, t) + if err != nil { + return nil, err + } + + return t, nil +} + func (o *TaskStageReadOptions) valid() error { if o == nil { return nil // nothing to validate @@ -140,6 +195,8 @@ func validateTaskStageIncludeParams(params []TaskStageIncludeOpt) error { switch p { case TaskStageTaskResults: // do nothing + case PolicyEvaluationsTaskResults: + // do nothing default: return ErrInvalidIncludeValue } diff --git a/task_stages_integration_beta_test.go b/task_stages_integration_beta_test.go new file mode 100644 index 000000000..ce3d85432 --- /dev/null +++ b/task_stages_integration_beta_test.go @@ -0,0 +1,260 @@ +//go:build integration +// +build integration + +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTaskStagesRead_Beta(t *testing.T) { + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + Path: String(".rego"), + Mode: EnforcementMode(EnforcementAdvisory), + }, + }, + } + policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options) + defer policyTestCleanup() + + policySet := []*Policy{policyTest} + _, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, OPA) + defer psTestCleanup1() + + wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + defer wrTaskTestCleanup() + + rTest, rTestCleanup := createRun(t, client, wkspaceTest) + defer rTestCleanup() + + r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, &RunReadOptions{ + Include: []RunIncludeOpt{RunTaskStages}, + }) + require.NoError(t, err) + require.NotEmpty(t, r.TaskStages) + require.NotNil(t, r.TaskStages[0]) + + t.Run("without read options", func(t *testing.T) { + taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, nil) + require.NoError(t, err) + + assert.NotEmpty(t, taskStage.ID) + assert.NotEmpty(t, taskStage.Stage) + assert.NotNil(t, taskStage.StatusTimestamps.ErroredAt) + assert.NotNil(t, taskStage.StatusTimestamps.RunningAt) + assert.NotNil(t, taskStage.CreatedAt) + assert.NotNil(t, taskStage.UpdatedAt) + assert.NotNil(t, taskStage.Run) + assert.NotNil(t, taskStage.TaskResults) + + // so this bit is interesting, if the relation is not specified in the include + // param, the fields of the struct will be zeroed out, minus the ID + assert.NotEmpty(t, taskStage.TaskResults[0].ID) + assert.Empty(t, taskStage.TaskResults[0].Status) + assert.Empty(t, taskStage.TaskResults[0].Message) + + assert.NotEmpty(t, taskStage.PolicyEvaluations[0].ID) + }) + + t.Run("with include param task_results", func(t *testing.T) { + taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, &TaskStageReadOptions{ + Include: []TaskStageIncludeOpt{TaskStageTaskResults, PolicyEvaluationsTaskResults}, + }) + require.NoError(t, err) + require.NotEmpty(t, taskStage.TaskResults) + require.NotNil(t, taskStage.TaskResults[0]) + require.NotEmpty(t, taskStage.PolicyEvaluations) + require.NotNil(t, taskStage.PolicyEvaluations[0]) + + t.Run("task results are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, taskStage.TaskResults[0].ID) + assert.NotEmpty(t, taskStage.TaskResults[0].Status) + assert.NotEmpty(t, taskStage.TaskResults[0].CreatedAt) + assert.Equal(t, wrTaskTest.ID, taskStage.TaskResults[0].WorkspaceTaskID) + assert.Equal(t, runTaskTest.Name, taskStage.TaskResults[0].TaskName) + }) + + t.Run("policy evaluations are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, taskStage.PolicyEvaluations[0].ID) + assert.NotEmpty(t, taskStage.PolicyEvaluations[0].Status) + assert.NotEmpty(t, taskStage.PolicyEvaluations[0].CreatedAt) + assert.Equal(t, OPA, taskStage.PolicyEvaluations[0].PolicyKind) + assert.NotEmpty(t, taskStage.PolicyEvaluations[0].UpdatedAt) + assert.NotNil(t, taskStage.PolicyEvaluations[0].ResultCount) + }) + + }) +} + +func TestTaskStagesList_Beta(t *testing.T) { + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + runTaskTest2, runTaskTest2Cleanup := createRunTask(t, client, orgTest) + defer runTaskTest2Cleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + Path: String(".rego"), + Mode: EnforcementMode(EnforcementAdvisory), + }, + }, + } + policyTest, policyTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options) + defer policyTestCleanup() + + policyTest2, policyTestCleanup2 := createUploadedPolicyWithOptions(t, client, true, orgTest, options) + defer policyTestCleanup2() + + policySet := []*Policy{policyTest, policyTest2} + _, psTestCleanup1 := createPolicySet(t, client, orgTest, policySet, []*Workspace{wkspaceTest}, OPA) + defer psTestCleanup1() + + policySet2 := []*Policy{policyTest2} + _, psTestCleanup2 := createPolicySet(t, client, orgTest, policySet2, []*Workspace{wkspaceTest}, OPA) + defer psTestCleanup2() + + _, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + defer wrTaskTestCleanup() + + _, wrTaskTest2Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest2) + defer wrTaskTest2Cleanup() + + rTest, rTestCleanup := createRun(t, client, wkspaceTest) + defer rTestCleanup() + + t.Run("with no params", func(t *testing.T) { + taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) + require.NoError(t, err) + + require.NotEmpty(t, taskStageList.Items) + assert.NotEmpty(t, taskStageList.Items[0].ID) + assert.Equal(t, 2, len(taskStageList.Items[0].TaskResults)) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + }) +} + +func TestTaskStageOverride_Beta(t *testing.T) { + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + t.Run("when the policy failed", func(t *testing.T) { + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + Mode: EnforcementMode(EnforcementMandatory), + }, + }, + } + pTest, pTestCleanup := createUploadedPolicyWithOptions(t, client, false, orgTest, options) + defer pTestCleanup() + + wTest, wTestCleanup := createWorkspace(t, client, orgTest) + defer wTestCleanup() + opts := PolicySetCreateOptions{ + Kind: OPA, + Overridable: Bool(true), + } + createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, opts) + rTest, tTestCleanup := createRunWaitForStatus(t, client, wTest, RunAwaitingDecision) + defer tTestCleanup() + + taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) + require.NoError(t, err) + + require.NotEmpty(t, taskStageList.Items) + assert.NotEmpty(t, taskStageList.Items[0].ID) + assert.Equal(t, TaskStageAwaitingOverride, taskStageList.Items[0].Status) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + + _, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID) + require.NoError(t, err) + }) + + t.Run("when the policy passed", func(t *testing.T) { + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + Mode: EnforcementMode(EnforcementMandatory), + }, + }, + } + pTest, pTestCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options) + defer pTestCleanup() + wTest, wTestCleanup := createWorkspace(t, client, orgTest) + defer wTestCleanup() + opts := PolicySetCreateOptions{ + Kind: OPA, + Overridable: Bool(true), + } + createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, opts) + rTest, tTestCleanup := createPlannedRun(t, client, wTest) + defer tTestCleanup() + + taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) + require.NoError(t, err) + + require.NotEmpty(t, taskStageList.Items) + assert.NotEmpty(t, taskStageList.Items[0].ID) + assert.Equal(t, TaskStagePassed, taskStageList.Items[0].Status) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + + _, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID) + assert.Errorf(t, err, "transition not allowed") + }) +} From ab2c72995cc8916f9c6b945f9a786d81fa26713a Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Thu, 10 Nov 2022 15:00:25 +1100 Subject: [PATCH 2/4] Add CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad283e97..b7208748a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add OPA support to the Policy Set APIs by @mrinalirao [#575](https://github.com/hashicorp/go-tfe/pull/575) * Add OPA support to the Policy APIs by @mrinalirao [#579](https://github.com/hashicorp/go-tfe/pull/579) * Add Policy Evaluation and Policy Set Outcome APIs by @mrinalirao [#583](https://github.com/hashicorp/go-tfe/pull/583) +* Add OPA support to Task Stage APIs by @mrinalirao [#584](https://github.com/hashicorp/go-tfe/pull/584) # v1.12.0 From 3d24aa67e801f2e1e3b888d6dfa3fec1983515b9 Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Fri, 11 Nov 2022 14:22:29 +1100 Subject: [PATCH 3/4] Pass optional comment to the override API + tests --- mocks/task_stages_mocks.go | 8 ++--- run.go | 52 ++++++++++++++-------------- task_stages.go | 12 +++++-- task_stages_integration_beta_test.go | 48 +++++++++++++++++++++++-- 4 files changed, 84 insertions(+), 36 deletions(-) diff --git a/mocks/task_stages_mocks.go b/mocks/task_stages_mocks.go index 47d470aa9..2c8b79451 100644 --- a/mocks/task_stages_mocks.go +++ b/mocks/task_stages_mocks.go @@ -51,18 +51,18 @@ func (mr *MockTaskStagesMockRecorder) List(ctx, runID, options interface{}) *gom } // Override mocks base method. -func (m *MockTaskStages) Override(ctx context.Context, taskStageID string) (*tfe.TaskStage, error) { +func (m *MockTaskStages) Override(ctx context.Context, taskStageID string, options tfe.TaskStageOverrideOptions) (*tfe.TaskStage, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Override", ctx, taskStageID) + ret := m.ctrl.Call(m, "Override", ctx, taskStageID, options) ret0, _ := ret[0].(*tfe.TaskStage) ret1, _ := ret[1].(error) return ret0, ret1 } // Override indicates an expected call of Override. -func (mr *MockTaskStagesMockRecorder) Override(ctx, taskStageID interface{}) *gomock.Call { +func (mr *MockTaskStagesMockRecorder) Override(ctx, taskStageID, options interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Override", reflect.TypeOf((*MockTaskStages)(nil).Override), ctx, taskStageID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Override", reflect.TypeOf((*MockTaskStages)(nil).Override), ctx, taskStageID, options) } // Read mocks base method. diff --git a/run.go b/run.go index 6655e2b23..22739824a 100644 --- a/run.go +++ b/run.go @@ -53,32 +53,32 @@ type RunStatus string // List all available run statuses. const ( - RunApplied RunStatus = "applied" - RunApplying RunStatus = "applying" - RunApplyQueued RunStatus = "apply_queued" - RunCanceled RunStatus = "canceled" - RunConfirmed RunStatus = "confirmed" - RunCostEstimated RunStatus = "cost_estimated" - RunCostEstimating RunStatus = "cost_estimating" - RunDiscarded RunStatus = "discarded" - RunErrored RunStatus = "errored" - RunFetching RunStatus = "fetching" - RunFetchingCompleted RunStatus = "fetching_completed" - RunPending RunStatus = "pending" - RunPlanned RunStatus = "planned" - RunPlannedAndFinished RunStatus = "planned_and_finished" - RunPlanning RunStatus = "planning" - RunPlanQueued RunStatus = "plan_queued" - RunPolicyChecked RunStatus = "policy_checked" - RunPolicyChecking RunStatus = "policy_checking" - RunPolicyOverride RunStatus = "policy_override" - RunPolicySoftFailed RunStatus = "policy_soft_failed" - RunPostPlanCompleted RunStatus = "post_plan_completed" - RunPostPlanRunning RunStatus = "post_plan_running" - RunPrePlanCompleted RunStatus = "pre_plan_completed" - RunPrePlanRunning RunStatus = "pre_plan_running" - RunQueuing RunStatus = "queuing" - RunAwaitingDecision RunStatus = "post_plan_awaiting_decision" + RunApplied RunStatus = "applied" + RunApplying RunStatus = "applying" + RunApplyQueued RunStatus = "apply_queued" + RunCanceled RunStatus = "canceled" + RunConfirmed RunStatus = "confirmed" + RunCostEstimated RunStatus = "cost_estimated" + RunCostEstimating RunStatus = "cost_estimating" + RunDiscarded RunStatus = "discarded" + RunErrored RunStatus = "errored" + RunFetching RunStatus = "fetching" + RunFetchingCompleted RunStatus = "fetching_completed" + RunPending RunStatus = "pending" + RunPlanned RunStatus = "planned" + RunPlannedAndFinished RunStatus = "planned_and_finished" + RunPlanning RunStatus = "planning" + RunPlanQueued RunStatus = "plan_queued" + RunPolicyChecked RunStatus = "policy_checked" + RunPolicyChecking RunStatus = "policy_checking" + RunPolicyOverride RunStatus = "policy_override" + RunPolicySoftFailed RunStatus = "policy_soft_failed" + RunPostPlanCompleted RunStatus = "post_plan_completed" + RunPostPlanRunning RunStatus = "post_plan_running" + RunPrePlanCompleted RunStatus = "pre_plan_completed" + RunPrePlanRunning RunStatus = "pre_plan_running" + RunQueuing RunStatus = "queuing" + RunPostPlanAwaitingDecision RunStatus = "post_plan_awaiting_decision" ) // RunSource represents a source type of a run. diff --git a/task_stages.go b/task_stages.go index 8d72eb05f..5cb81ee0d 100644 --- a/task_stages.go +++ b/task_stages.go @@ -20,7 +20,7 @@ type TaskStages interface { // **Note: This function is still in BETA and subject to change.** // Override a task stage for a given run - Override(ctx context.Context, taskStageID string) (*TaskStage, error) + Override(ctx context.Context, taskStageID string, options TaskStageOverrideOptions) (*TaskStage, error) } // taskStages implements TaskStages @@ -76,6 +76,12 @@ type TaskStage struct { PolicyEvaluations []*PolicyEvaluation `jsonapi:"relation,policy-evaluations"` } +// TaskStageOverrideOptions represents the options for overriding a TaskStage. +type TaskStageOverrideOptions struct { + // An optional explanation for why the stage was overridden + Comment *string `json:"comment,omitempty"` +} + // TaskStageList represents a list of task stages type TaskStageList struct { *Pagination @@ -158,13 +164,13 @@ func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageL // **Note: This function is still in BETA and subject to change.** // Override a task stages for a run -func (s *taskStages) Override(ctx context.Context, taskStageID string) (*TaskStage, error) { +func (s *taskStages) Override(ctx context.Context, taskStageID string, options TaskStageOverrideOptions) (*TaskStage, error) { if !validStringID(&taskStageID) { return nil, ErrInvalidTaskStageID } u := fmt.Sprintf("task-stages/%s/actions/override", taskStageID) - req, err := s.client.NewRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } diff --git a/task_stages_integration_beta_test.go b/task_stages_integration_beta_test.go index ce3d85432..cb1f097e6 100644 --- a/task_stages_integration_beta_test.go +++ b/task_stages_integration_beta_test.go @@ -205,7 +205,7 @@ func TestTaskStageOverride_Beta(t *testing.T) { Overridable: Bool(true), } createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, opts) - rTest, tTestCleanup := createRunWaitForStatus(t, client, wTest, RunAwaitingDecision) + rTest, tTestCleanup := createRunWaitForStatus(t, client, wTest, RunPostPlanAwaitingDecision) defer tTestCleanup() taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) @@ -216,7 +216,49 @@ func TestTaskStageOverride_Beta(t *testing.T) { assert.Equal(t, TaskStageAwaitingOverride, taskStageList.Items[0].Status) assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) - _, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID) + _, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID, TaskStageOverrideOptions{}) + require.NoError(t, err) + }) + + t.Run("when the policy failed with options", func(t *testing.T) { + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + Mode: EnforcementMode(EnforcementMandatory), + }, + }, + } + pTest, pTestCleanup := createUploadedPolicyWithOptions(t, client, false, orgTest, options) + defer pTestCleanup() + + wTest, wTestCleanup := createWorkspace(t, client, orgTest) + defer wTestCleanup() + opts := PolicySetCreateOptions{ + Kind: OPA, + Overridable: Bool(true), + } + createPolicySetWithOptions(t, client, orgTest, []*Policy{pTest}, []*Workspace{wTest}, opts) + rTest, tTestCleanup := createRunWaitForStatus(t, client, wTest, RunPostPlanAwaitingDecision) + defer tTestCleanup() + + taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) + require.NoError(t, err) + + require.NotEmpty(t, taskStageList.Items) + assert.NotEmpty(t, taskStageList.Items[0].ID) + assert.Equal(t, TaskStageAwaitingOverride, taskStageList.Items[0].Status) + assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) + + taskStageOverrideOptions := TaskStageOverrideOptions{ + Comment: String("test comment"), + } + ts, err := client.TaskStages.Override(ctx, taskStageList.Items[0].ID, taskStageOverrideOptions) require.NoError(t, err) }) @@ -254,7 +296,7 @@ func TestTaskStageOverride_Beta(t *testing.T) { assert.Equal(t, TaskStagePassed, taskStageList.Items[0].Status) assert.Equal(t, 1, len(taskStageList.Items[0].PolicyEvaluations)) - _, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID) + _, err = client.TaskStages.Override(ctx, taskStageList.Items[0].ID, TaskStageOverrideOptions{}) assert.Errorf(t, err, "transition not allowed") }) } From 34922c894e19adc36406ec493e6b90e21d85988b Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Tue, 15 Nov 2022 09:12:45 +1100 Subject: [PATCH 4/4] minor fixes --- run.go | 2 +- task_stages.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/run.go b/run.go index 22739824a..dad081898 100644 --- a/run.go +++ b/run.go @@ -73,12 +73,12 @@ const ( RunPolicyChecking RunStatus = "policy_checking" RunPolicyOverride RunStatus = "policy_override" RunPolicySoftFailed RunStatus = "policy_soft_failed" + RunPostPlanAwaitingDecision RunStatus = "post_plan_awaiting_decision" RunPostPlanCompleted RunStatus = "post_plan_completed" RunPostPlanRunning RunStatus = "post_plan_running" RunPrePlanCompleted RunStatus = "pre_plan_completed" RunPrePlanRunning RunStatus = "pre_plan_running" RunQueuing RunStatus = "queuing" - RunPostPlanAwaitingDecision RunStatus = "post_plan_awaiting_decision" ) // RunSource represents a source type of a run. diff --git a/task_stages.go b/task_stages.go index 5cb81ee0d..a707cbfce 100644 --- a/task_stages.go +++ b/task_stages.go @@ -57,7 +57,7 @@ type Permissions struct { // Actions represents a task stage actions type Actions struct { - IsOverridable *bool `jsonapi:"attr, overridable"` + IsOverridable *bool `jsonapi:"attr,is-overridable"` } // TaskStage represents a TFC/E run's stage where run tasks can occur