From f51eb43270b1f40c7360b591318b935259dbe575 Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Thu, 10 Nov 2022 12:17:34 +1100 Subject: [PATCH 1/3] Add API's for: - List Policy evaluations in a task stage - List/Read Policy set outcomes in a policy evaluation --- errors.go | 4 + generate_mocks.sh | 1 + helper_test.go | 25 +++- mocks/policy_evaluation.go | 104 ++++++++++++++ policy_evaluation.go | 230 ++++++++++++++++++++++++++++++ policy_evaluation_beta_test.go | 248 +++++++++++++++++++++++++++++++++ task_stages.go | 5 +- tfe.go | 20 ++- 8 files changed, 625 insertions(+), 12 deletions(-) create mode 100644 mocks/policy_evaluation.go create mode 100644 policy_evaluation.go create mode 100644 policy_evaluation_beta_test.go diff --git a/errors.go b/errors.go index 31371e7da..73b3bf177 100644 --- a/errors.go +++ b/errors.go @@ -116,6 +116,10 @@ var ( ErrInvalidPolicyCheckID = errors.New("invalid value for policy check ID") + ErrInvalidPolicyEvaluationID = errors.New("invalid value for policy evaluation ID") + + ErrInvalidPolicySetOutcomeID = errors.New("invalid value for policy set outcome ID") + ErrInvalidTag = errors.New("invalid tag id") ErrInvalidPlanExportID = errors.New("invalid value for plan export ID") diff --git a/generate_mocks.sh b/generate_mocks.sh index e2beaf1d3..9a410c7ee 100755 --- a/generate_mocks.sh +++ b/generate_mocks.sh @@ -61,3 +61,4 @@ mockgen -source=variable_set_variable.go -destination=mocks/variable_set_variabl mockgen -source=workspace.go -destination=mocks/workspace_mocks.go -package=mocks mockgen -source=workspace_run_task.go -destination=mocks/workspace_run_tasks_mocks.go -package=mocks mockgen -source=agent.go -destination=mocks/agents.go -package=mocks +mockgen -source=policy_evaluation.go -destination=mocks/policy_evaluation.go -package=mocks diff --git a/helper_test.go b/helper_test.go index 7151929ee..c8436db5b 100644 --- a/helper_test.go +++ b/helper_test.go @@ -622,11 +622,19 @@ func createPolicyWithOptions(t *testing.T, client *Client, org *Organization, op } name := randomString(t) + path := name + ".sentinel" + if opts.Kind == OPA { + path = name + ".rego" + } options := PolicyCreateOptions{ - Name: String(name), - Kind: opts.Kind, - Query: opts.Query, - Enforce: opts.Enforce, + Name: String(name), + Kind: opts.Kind, + Query: opts.Query, + Enforce: []*EnforcementOptions{ + { + Path: String(path), + Mode: opts.Enforce[0].Mode}, + }, } ctx := context.Background() @@ -687,7 +695,14 @@ func createUploadedPolicyWithOptions(t *testing.T, client *Client, pass bool, or p, pCleanup := createPolicyWithOptions(t, client, org, opts) ctx := context.Background() - err := client.Policies.Upload(ctx, p.ID, []byte(fmt.Sprintf("main = rule { %t }", pass))) + policy := fmt.Sprintf("main = rule { %t }", pass) + if opts.Kind == OPA { + policy = `package example rule["not allowed"] { false }` + if !pass { + policy = `package example rule["not allowed"] { true }` + } + } + err := client.Policies.Upload(ctx, p.ID, []byte(policy)) if err != nil { t.Fatal(err) } diff --git a/mocks/policy_evaluation.go b/mocks/policy_evaluation.go new file mode 100644 index 000000000..d1f7741a9 --- /dev/null +++ b/mocks/policy_evaluation.go @@ -0,0 +1,104 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: policy_evaluation.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tfe "github.com/hashicorp/go-tfe" +) + +// MockPolicyEvaluations is a mock of PolicyEvaluations interface. +type MockPolicyEvaluations struct { + ctrl *gomock.Controller + recorder *MockPolicyEvaluationsMockRecorder +} + +// MockPolicyEvaluationsMockRecorder is the mock recorder for MockPolicyEvaluations. +type MockPolicyEvaluationsMockRecorder struct { + mock *MockPolicyEvaluations +} + +// NewMockPolicyEvaluations creates a new mock instance. +func NewMockPolicyEvaluations(ctrl *gomock.Controller) *MockPolicyEvaluations { + mock := &MockPolicyEvaluations{ctrl: ctrl} + mock.recorder = &MockPolicyEvaluationsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPolicyEvaluations) EXPECT() *MockPolicyEvaluationsMockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockPolicyEvaluations) List(ctx context.Context, taskStageID string, options *tfe.PolicyEvaluationListOptions) (*tfe.PolicyEvaluationList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, taskStageID, options) + ret0, _ := ret[0].(*tfe.PolicyEvaluationList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockPolicyEvaluationsMockRecorder) List(ctx, taskStageID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicyEvaluations)(nil).List), ctx, taskStageID, options) +} + +// MockPolicySetOutcomes is a mock of PolicySetOutcomes interface. +type MockPolicySetOutcomes struct { + ctrl *gomock.Controller + recorder *MockPolicySetOutcomesMockRecorder +} + +// MockPolicySetOutcomesMockRecorder is the mock recorder for MockPolicySetOutcomes. +type MockPolicySetOutcomesMockRecorder struct { + mock *MockPolicySetOutcomes +} + +// NewMockPolicySetOutcomes creates a new mock instance. +func NewMockPolicySetOutcomes(ctrl *gomock.Controller) *MockPolicySetOutcomes { + mock := &MockPolicySetOutcomes{ctrl: ctrl} + mock.recorder = &MockPolicySetOutcomesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPolicySetOutcomes) EXPECT() *MockPolicySetOutcomesMockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockPolicySetOutcomes) List(ctx context.Context, policyEvaluationID string, options *tfe.PolicySetOutcomeListOptions) (*tfe.PolicySetOutcomeList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, policyEvaluationID, options) + ret0, _ := ret[0].(*tfe.PolicySetOutcomeList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockPolicySetOutcomesMockRecorder) List(ctx, policyEvaluationID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPolicySetOutcomes)(nil).List), ctx, policyEvaluationID, options) +} + +// Read mocks base method. +func (m *MockPolicySetOutcomes) Read(ctx context.Context, policy_set_outcome_id string) (*tfe.PolicySetOutcome, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, policy_set_outcome_id) + ret0, _ := ret[0].(*tfe.PolicySetOutcome) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockPolicySetOutcomesMockRecorder) Read(ctx, policy_set_outcome_id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySetOutcomes)(nil).Read), ctx, policy_set_outcome_id) +} diff --git a/policy_evaluation.go b/policy_evaluation.go new file mode 100644 index 000000000..2b43a5d8b --- /dev/null +++ b/policy_evaluation.go @@ -0,0 +1,230 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" + "time" +) + +// Compile-time proof of interface implementation. +var _ PolicyEvaluations = (*policyEvaluation)(nil) + +// PolicyResultCount represents the count of the policy results +type PolicyResultCount struct { + AdvisoryFailed int `jsonapi:"attr,advisory-failed"` + MandatoryFailed int `jsonapi:"attr,mandatory-failed"` + Passed int `jsonapi:"attr,passed"` + Errored int `jsonapi:"attr,errored"` +} + +// The task stage the policy evaluation belongs to +type PolicyAttachable struct { + ID string `jsonapi:"attr,id"` + Type string `jsonapi:"attr,type"` +} + +// PolicyEvaluation represents the policy evaluations that are part of the task stage. +type PolicyEvaluation struct { + ID string `jsonapi:"primary,policy-evaluations"` + Status TaskResultStatus `jsonapi:"attr,status"` + PolicyKind PolicyKind `jsonapi:"attr,policy-kind"` + StatusTimestamps TaskResultStatusTimestamps `jsonapi:"attr,status-timestamps"` + ResultCount *PolicyResultCount `jsonapi:"attr,result-count"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + + // The task stage this evaluation belongs to + TaskStage *PolicyAttachable `jsonapi:"relation,policy-attachable"` +} + +// PolicyEvalutations describes all the policy evaluation related methods that the +// Terraform Enterprise API supports. +// +// TFE API docs: +// https://www.terraform.io/docs/cloud/api/policy-checks.html +type PolicyEvaluations interface { + // **Note: This method is still in BETA and subject to change.** + // List all policy evaluations in the task stage. Only available for OPA policies. + List(ctx context.Context, taskStageID string, options *PolicyEvaluationListOptions) (*PolicyEvaluationList, error) +} + +// policyEvaluation implements PolicyEvaluations. +type policyEvaluation struct { + client *Client +} + +// PolicyEvaluationListOptions represents the options for listing policy evaluations. +type PolicyEvaluationListOptions struct { + ListOptions +} + +// PolicyEvaluationList represents a list of policy evaluation. +type PolicyEvaluationList struct { + *Pagination + Items []*PolicyEvaluation +} + +// List all policy evaluations in a task stage. +func (s *policyEvaluation) List(ctx context.Context, taskStageID string, options *PolicyEvaluationListOptions) (*PolicyEvaluationList, error) { + if !validStringID(&taskStageID) { + return nil, ErrInvalidTaskStageID + } + + u := fmt.Sprintf("task-stages/%s/policy-evaluations", url.QueryEscape(taskStageID)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + pcl := &PolicyEvaluationList{} + err = req.Do(ctx, pcl) + if err != nil { + return nil, err + } + + return pcl, nil +} + +// Compile-time proof of interface implementation. +var _ PolicySetOutcomes = (*policySetOutcome)(nil) + +// PolicySetOutcomes describes all the policy set outcome related methods that the +// Terraform Enterprise API supports. +// +// TFE API docs: +// https://www.terraform.io/docs/cloud/api/policy-checks.html +type PolicySetOutcomes interface { + // **Note: This method is still in BETA and subject to change.** + // List all policy set outcomes in the policy evaluation. Only available for OPA policies. + List(ctx context.Context, policyEvaluationID string, options *PolicySetOutcomeListOptions) (*PolicySetOutcomeList, error) + + // **Note: This method is still in BETA and subject to change.** + // Read a policy set outcome by its ID. Only available for OPA policies. + Read(ctx context.Context, policy_set_outcome_id string) (*PolicySetOutcome, error) +} + +// policySetOutcome implements PolicySetOutcomes. +type policySetOutcome struct { + client *Client +} + +// PolicySetOutcomeListFilter represents the filters that are supported while listing a policy set outcome +type PolicySetOutcomeListFilter struct { + // Optional: A status string used to filter the results. + // Must be either "passed", "failed", or "errored". + Status string + + // Optional: The enforcement level used to filter the results. + // Must be either "advisory" or "mandatory". + EnforcementLevel string +} + +// PolicySetOutcomeListOptions represents the options for listing policy set outcomes. +type PolicySetOutcomeListOptions struct { + *ListOptions + + // Optional: A filter map used to filter the results of the policy outcome. + // You can use filter[n] to combine combinations of statuses and enforcement levels filters + Filter map[string]PolicySetOutcomeListFilter +} + +// PolicySetOutcomeList represents a list of policy set outcomes. +type PolicySetOutcomeList struct { + *Pagination + Items []*PolicySetOutcome +} + +// Outcome represents the outcome of the individual policy +type Outcome struct { + EnforcementLevel EnforcementLevel `jsonapi:"attr,enforcement_level"` + Query string `jsonapi:"attr,query"` + Status string `jsonapi:"attr,status"` + PolicyName string `jsonapi:"attr,policy_name"` + Description string `jsonapi:"attr,description"` +} + +// PolicySetOutcome represents outcome of the policy set that are part of the policy evaluation +type PolicySetOutcome struct { + ID string `jsonapi:"primary,policy-set-outcomes"` + Outcomes []Outcome `jsonapi:"attr,outcomes"` + Error string `jsonapi:"attr,error"` + Overridable *bool `jsonapi:"attr,overridable"` + PolicySetName string `jsonapi:"attr,policy-set-name"` + PolicySetDescription string `jsonapi:"attr,policy-set-description"` + ResultCount PolicyResultCount `jsonapi:"attr,result_count"` + + // The policy evaluation that this outcome belongs to + PolicyEvaluation *PolicyEvaluation `jsonapi:"relation,policy-evaluation"` +} + +// List all policy set outcomes in a policy evaluation. +func (s *policySetOutcome) List(ctx context.Context, policyEvaluationID string, options *PolicySetOutcomeListOptions) (*PolicySetOutcomeList, error) { + if !validStringID(&policyEvaluationID) { + return nil, ErrInvalidPolicyEvaluationID + } + + additionalQueryParams := options.buildQueryString() + + u := fmt.Sprintf("policy-evaluations/%s/policy-set-outcomes", url.QueryEscape(policyEvaluationID)) + + var opts *ListOptions + if options != nil && options.ListOptions != nil { + opts = options.ListOptions + } + + req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, opts, additionalQueryParams) + if err != nil { + return nil, err + } + + psol := &PolicySetOutcomeList{} + err = req.Do(ctx, psol) + if err != nil { + return nil, err + } + + return psol, nil +} + +// buildQueryString takes the PolicySetOutcomeListOptions and returns a filters map. +// This function is required due to the limitations of the current library, +// we cannot encode map of objects using the current library that is used by go-tfe: https://github.com/google/go-querystring/issues/7 +func (opts *PolicySetOutcomeListOptions) buildQueryString() map[string][]string { + result := make(map[string][]string) + if opts == nil || opts.Filter == nil { + return nil + } + for k, v := range opts.Filter { + if v.Status != "" { + newKey := fmt.Sprintf("filter[%s][status]", k) + result[newKey] = append(result[newKey], v.Status) + } + if v.EnforcementLevel != "" { + newKey := fmt.Sprintf("filter[%s][enforcement_level]", k) + result[newKey] = append(result[newKey], v.EnforcementLevel) + } + } + return result +} + +// Read reads a policy set outcome by its ID +func (s *policySetOutcome) Read(ctx context.Context, policySetOutcomeID string) (*PolicySetOutcome, error) { + if !validStringID(&policySetOutcomeID) { + return nil, ErrInvalidPolicySetOutcomeID + } + + u := fmt.Sprintf("policy-set-outcomes/%s", url.QueryEscape(policySetOutcomeID)) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + pso := &PolicySetOutcome{} + err = req.Do(ctx, pso) + if err != nil { + return nil, err + } + + return pso, err +} diff --git a/policy_evaluation_beta_test.go b/policy_evaluation_beta_test.go new file mode 100644 index 000000000..3603e8649 --- /dev/null +++ b/policy_evaluation_beta_test.go @@ -0,0 +1,248 @@ +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPolicyEvaluationList_Beta(t *testing.T) { + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + 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() + + 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, 1, len(taskStageList.Items[0].PolicyEvaluation)) + + polEvaluation, err := client.PolicyEvaluations.List(ctx, taskStageList.Items[0].ID, nil) + require.NoError(t, err) + + require.NotEmpty(t, polEvaluation.Items) + assert.NotEmpty(t, polEvaluation.Items[0].ID) + }) + + t.Run("with a invalid policy evaluation ID", func(t *testing.T) { + + policyEvaluationeID := "invalid ID" + + _, err := client.PolicyEvaluations.List(ctx, policyEvaluationeID, nil) + require.Errorf(t, err, "invalid value for policy evaluation ID") + }) +} + +func TestPolicySetOutcomeList_Beta(t *testing.T) { + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + 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() + + rTest, rTestCleanup := createPlannedRun(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, 1, len(taskStageList.Items[0].PolicyEvaluation)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + + polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + + polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, nil) + require.NoError(t, err) + + require.NotEmpty(t, polSetOutcomesList.Items) + assert.NotEmpty(t, polSetOutcomesList.Items[0].ID) + assert.NotEmpty(t, polSetOutcomesList.Items[0].Outcomes) + assert.NotEmpty(t, polSetOutcomesList.Items[0].PolicySetName) + }) + + t.Run("with non-matching filters", 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, 1, len(taskStageList.Items[0].PolicyEvaluation)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + + polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + + opts := &PolicySetOutcomeListOptions{ + Filter: map[string]PolicySetOutcomeListFilter{ + "0": { + Status: "errored", + }, + "1": { + EnforcementLevel: "mandatory", + Status: "failed", + }, + }, + } + + polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, opts) + require.NoError(t, err) + + require.Empty(t, polSetOutcomesList.Items) + }) + + t.Run("with matching filters", 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, 1, len(taskStageList.Items[0].PolicyEvaluation)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + + polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + + opts := &PolicySetOutcomeListOptions{ + Filter: map[string]PolicySetOutcomeListFilter{ + "0": { + Status: "passed", + EnforcementLevel: "advisory", + }, + }, + } + + polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, opts) + require.NoError(t, err) + + require.NotEmpty(t, polSetOutcomesList.Items) + assert.NotEmpty(t, polSetOutcomesList.Items[0].ID) + assert.Equal(t, 1, len(polSetOutcomesList.Items[0].Outcomes)) + assert.NotEmpty(t, polSetOutcomesList.Items[0].PolicySetName) + }) +} + +func TestPolicySetOutcomeRead_Beta(t *testing.T) { + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + options := PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("data.example.rule"), + Enforce: []*EnforcementOptions{ + { + 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() + + rTest, rTestCleanup := createPlannedRun(t, client, wkspaceTest) + defer rTestCleanup() + + t.Run("with a valid policy set outcome ID", 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, 1, len(taskStageList.Items[0].PolicyEvaluation)) + assert.NotEmpty(t, 1, len(taskStageList.Items[0].PolicyEvaluation[0].ID)) + + polEvaluationID := taskStageList.Items[0].PolicyEvaluation[0].ID + + polSetOutcomesList, err := client.PolicySetOutcomes.List(ctx, polEvaluationID, nil) + require.NoError(t, err) + + require.NotEmpty(t, polSetOutcomesList.Items) + assert.NotEmpty(t, polSetOutcomesList.Items[0].ID) + assert.NotEmpty(t, polSetOutcomesList.Items[0].Outcomes) + assert.NotEmpty(t, polSetOutcomesList.Items[0].PolicySetName) + + policySetOutcomeID := polSetOutcomesList.Items[0].ID + + policyOutcome, err := client.PolicySetOutcomes.Read(ctx, policySetOutcomeID) + require.NoError(t, err) + + assert.NotEmpty(t, policyOutcome.ID) + assert.NotEmpty(t, policyOutcome.Outcomes) + }) + + t.Run("with a invalid policy set outcome ID", func(t *testing.T) { + + policySetOutcomeID := "invalid ID" + + _, err := client.PolicySetOutcomes.Read(ctx, policySetOutcomeID) + require.Errorf(t, err, "invalid value for policy set outcome ID") + }) +} diff --git a/task_stages.go b/task_stages.go index 80974cc86..059436717 100644 --- a/task_stages.go +++ b/task_stages.go @@ -41,8 +41,9 @@ type TaskStage struct { CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - Run *Run `jsonapi:"relation,run"` - TaskResults []*TaskResult `jsonapi:"relation,task-results"` + Run *Run `jsonapi:"relation,run"` + TaskResults []*TaskResult `jsonapi:"relation,task-results"` + PolicyEvaluation []*PolicyEvaluation `jsonapi:"relation,policy-evaluations"` } // TaskStageList represents a list of task stages diff --git a/tfe.go b/tfe.go index 189ba2a20..2c155dd93 100644 --- a/tfe.go +++ b/tfe.go @@ -1,21 +1,20 @@ package tfe import ( - "errors" - "io/fs" - "log" - "sort" - "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "io/fs" + "log" "math/rand" "net/http" "net/url" "os" "reflect" + "sort" "strconv" "strings" "time" @@ -139,6 +138,8 @@ type Client struct { PlanExports PlanExports Policies Policies PolicyChecks PolicyChecks + PolicyEvaluations PolicyEvaluations + PolicySetOutcomes PolicySetOutcomes PolicySetParameters PolicySetParameters PolicySetVersions PolicySetVersions PolicySets PolicySets @@ -187,6 +188,10 @@ type Meta struct { } func (c *Client) NewRequest(method, path string, reqAttr interface{}) (*ClientRequest, error) { + return c.NewRequestWithAdditionalQueryParams(method, path, reqAttr, nil) +} + +func (c *Client) NewRequestWithAdditionalQueryParams(method, path string, reqAttr interface{}, additionalQueryParams map[string][]string) (*ClientRequest, error) { var u *url.URL var err error if strings.Contains(path, "/api/registry/") { @@ -215,6 +220,9 @@ func (c *Client) NewRequest(method, path string, reqAttr interface{}) (*ClientRe if err != nil { return nil, err } + for k, v := range additionalQueryParams { + q[k] = v + } u.RawQuery = encodeQueryParams(q) } case "DELETE", "PATCH", "POST": @@ -377,6 +385,8 @@ func NewClient(cfg *Config) (*Client, error) { client.Plans = &plans{client: client} client.Policies = &policies{client: client} client.PolicyChecks = &policyChecks{client: client} + client.PolicyEvaluations = &policyEvaluation{client: client} + client.PolicySetOutcomes = &policySetOutcome{client: client} client.PolicySetParameters = &policySetParameters{client: client} client.PolicySets = &policySets{client: client} client.PolicySetVersions = &policySetVersions{client: client} From 359a32e012918f4eb14b49824ece65c08e07500b Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Thu, 10 Nov 2022 13:59:12 +1100 Subject: [PATCH 2/3] Add policy evaluation statuses and timestamps + minor fixes --- helper_test.go | 3 ++- policy_evaluation.go | 37 +++++++++++++++++++++++++++------- policy_evaluation_beta_test.go | 26 ++++++++++++------------ task_stages.go | 6 +++--- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/helper_test.go b/helper_test.go index c8436db5b..6d0175237 100644 --- a/helper_test.go +++ b/helper_test.go @@ -633,7 +633,8 @@ func createPolicyWithOptions(t *testing.T, client *Client, org *Organization, op Enforce: []*EnforcementOptions{ { Path: String(path), - Mode: opts.Enforce[0].Mode}, + Mode: opts.Enforce[0].Mode, + }, }, } diff --git a/policy_evaluation.go b/policy_evaluation.go index 2b43a5d8b..fab5049f3 100644 --- a/policy_evaluation.go +++ b/policy_evaluation.go @@ -10,6 +10,20 @@ import ( // Compile-time proof of interface implementation. var _ PolicyEvaluations = (*policyEvaluation)(nil) +// PolicyEvaluationStatus is an enum that represents all possible statuses for a policy evaluation +type PolicyEvaluationStatus string + +const ( + PolicyEvaluationPassed PolicyEvaluationStatus = "passed" + PolicyEvaluationFailed PolicyEvaluationStatus = "failed" + PolicyEvaluationPending PolicyEvaluationStatus = "pending" + PolicyEvaluationRunning PolicyEvaluationStatus = "running" + PolicyEvaluationUnreachable PolicyEvaluationStatus = "unreachable" + PolicyEvaluationOverridden PolicyEvaluationStatus = "overridden" + PolicyEvaluationCanceled PolicyEvaluationStatus = "canceled" + PolicyEvaluationErrored PolicyEvaluationStatus = "errored" +) + // PolicyResultCount represents the count of the policy results type PolicyResultCount struct { AdvisoryFailed int `jsonapi:"attr,advisory-failed"` @@ -24,15 +38,24 @@ type PolicyAttachable struct { Type string `jsonapi:"attr,type"` } +// PolicyEvaluationStatusTimestamps represents the set of timestamps recorded for a policy evaluation +type PolicyEvaluationStatusTimestamps struct { + ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` + RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` + CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"` + FailedAt time.Time `jsonapi:"attr,failed-at,rfc3339"` + PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"` +} + // PolicyEvaluation represents the policy evaluations that are part of the task stage. type PolicyEvaluation struct { - ID string `jsonapi:"primary,policy-evaluations"` - Status TaskResultStatus `jsonapi:"attr,status"` - PolicyKind PolicyKind `jsonapi:"attr,policy-kind"` - StatusTimestamps TaskResultStatusTimestamps `jsonapi:"attr,status-timestamps"` - ResultCount *PolicyResultCount `jsonapi:"attr,result-count"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + ID string `jsonapi:"primary,policy-evaluations"` + Status PolicyEvaluationStatus `jsonapi:"attr,status"` + PolicyKind PolicyKind `jsonapi:"attr,policy-kind"` + StatusTimestamps PolicyEvaluationStatusTimestamps `jsonapi:"attr,status-timestamps"` + ResultCount *PolicyResultCount `jsonapi:"attr,result-count"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` // The task stage this evaluation belongs to TaskStage *PolicyAttachable `jsonapi:"relation,policy-attachable"` 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/task_stages.go b/task_stages.go index 059436717..4970ac195 100644 --- a/task_stages.go +++ b/task_stages.go @@ -41,9 +41,9 @@ type TaskStage struct { CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - 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 From e9f6b081efb6bf3e4d222a4bf32ca9152328f2bf Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Thu, 10 Nov 2022 14:54:22 +1100 Subject: [PATCH 3/3] Add CHANGELOG --- CHANGELOG.md | 1 + mocks/policy_evaluation.go | 8 ++++---- policy_evaluation.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac628b5b1..2ad283e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,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) # v1.12.0 diff --git a/mocks/policy_evaluation.go b/mocks/policy_evaluation.go index d1f7741a9..5ee675825 100644 --- a/mocks/policy_evaluation.go +++ b/mocks/policy_evaluation.go @@ -89,16 +89,16 @@ func (mr *MockPolicySetOutcomesMockRecorder) List(ctx, policyEvaluationID, optio } // Read mocks base method. -func (m *MockPolicySetOutcomes) Read(ctx context.Context, policy_set_outcome_id string) (*tfe.PolicySetOutcome, error) { +func (m *MockPolicySetOutcomes) Read(ctx context.Context, policySetOutcomeID string) (*tfe.PolicySetOutcome, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Read", ctx, policy_set_outcome_id) + ret := m.ctrl.Call(m, "Read", ctx, policySetOutcomeID) ret0, _ := ret[0].(*tfe.PolicySetOutcome) ret1, _ := ret[1].(error) return ret0, ret1 } // Read indicates an expected call of Read. -func (mr *MockPolicySetOutcomesMockRecorder) Read(ctx, policy_set_outcome_id interface{}) *gomock.Call { +func (mr *MockPolicySetOutcomesMockRecorder) Read(ctx, policySetOutcomeID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySetOutcomes)(nil).Read), ctx, policy_set_outcome_id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPolicySetOutcomes)(nil).Read), ctx, policySetOutcomeID) } diff --git a/policy_evaluation.go b/policy_evaluation.go index fab5049f3..f05a772c6 100644 --- a/policy_evaluation.go +++ b/policy_evaluation.go @@ -124,7 +124,7 @@ type PolicySetOutcomes interface { // **Note: This method is still in BETA and subject to change.** // Read a policy set outcome by its ID. Only available for OPA policies. - Read(ctx context.Context, policy_set_outcome_id string) (*PolicySetOutcome, error) + Read(ctx context.Context, policySetOutcomeID string) (*PolicySetOutcome, error) } // policySetOutcome implements PolicySetOutcomes.