From 9388b7a59a16a6af024e4fb9e9ddea1f187defcf Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Tue, 11 Apr 2023 16:31:02 +0800 Subject: [PATCH 1/3] Add support for querying run events This commit adds API support for querying the List and Read of run-events. --- README.md | 1 + errors.go | 2 + generate_mocks.sh | 1 + mocks/run_events_mocks.go | 81 +++++++++++++++++ run_event.go | 162 ++++++++++++++++++++++++++++++++++ run_event_integration_test.go | 129 +++++++++++++++++++++++++++ tfe.go | 2 + 7 files changed, 378 insertions(+) create mode 100644 mocks/run_events_mocks.go create mode 100644 run_event.go create mode 100644 run_event_integration_test.go diff --git a/README.md b/README.md index 786b42743..aa190dfa5 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ This API client covers most of the existing Terraform Cloud API calls and is upd - [x] GPG Keys - [x] Projects - [x] Runs +- [x] Run Events - [x] Run Tasks - [ ] Run Tasks Integration - [x] Run Triggers diff --git a/errors.go b/errors.go index 368a8fb70..74cced901 100644 --- a/errors.go +++ b/errors.go @@ -85,6 +85,8 @@ var ( ErrInvalidRunID = errors.New("invalid value for run ID") + ErrInvalidRunEventID = errors.New("invalid value for run event ID") + ErrInvalidProjectID = errors.New("invalid value for project ID") ErrInvalidPagination = errors.New("invalid value for page size or number") diff --git a/generate_mocks.sh b/generate_mocks.sh index 01dabd68c..d421d6d5e 100755 --- a/generate_mocks.sh +++ b/generate_mocks.sh @@ -44,6 +44,7 @@ mockgen -source=registry_provider.go -destination=mocks/registry_provider_mocks. mockgen -source=registry_provider_platform.go -destination=mocks/registry_provider_platform_mocks.go -package=mocks mockgen -source=registry_provider_version.go -destination=mocks/registry_provider_version_mocks.go -package=mocks mockgen -source=run.go -destination=mocks/run_mocks.go -package=mocks +mockgen -source=run_event.go -destination=mocks/run_events_mocks.go -package=mocks mockgen -source=run_task.go -destination=mocks/run_tasks_mocks.go -package=mocks mockgen -source=run_trigger.go -destination=mocks/run_trigger_mocks.go -package=mocks mockgen -source=ssh_key.go -destination=mocks/ssh_key_mocks.go -package=mocks diff --git a/mocks/run_events_mocks.go b/mocks/run_events_mocks.go new file mode 100644 index 000000000..1e7cd73f1 --- /dev/null +++ b/mocks/run_events_mocks.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: run_event.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" +) + +// MockRunEvents is a mock of RunEvents interface. +type MockRunEvents struct { + ctrl *gomock.Controller + recorder *MockRunEventsMockRecorder +} + +// MockRunEventsMockRecorder is the mock recorder for MockRunEvents. +type MockRunEventsMockRecorder struct { + mock *MockRunEvents +} + +// NewMockRunEvents creates a new mock instance. +func NewMockRunEvents(ctrl *gomock.Controller) *MockRunEvents { + mock := &MockRunEvents{ctrl: ctrl} + mock.recorder = &MockRunEventsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRunEvents) EXPECT() *MockRunEventsMockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockRunEvents) List(ctx context.Context, runID string, options *tfe.RunEventListOptions) (*tfe.RunEventList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, runID, options) + ret0, _ := ret[0].(*tfe.RunEventList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockRunEventsMockRecorder) List(ctx, runID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRunEvents)(nil).List), ctx, runID, options) +} + +// Read mocks base method. +func (m *MockRunEvents) Read(ctx context.Context, runEventID string) (*tfe.RunEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, runEventID) + ret0, _ := ret[0].(*tfe.RunEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockRunEventsMockRecorder) Read(ctx, runEventID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRunEvents)(nil).Read), ctx, runEventID) +} + +// ReadWithOptions mocks base method. +func (m *MockRunEvents) ReadWithOptions(ctx context.Context, runEventID string, options *tfe.RunEventReadOptions) (*tfe.RunEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadWithOptions", ctx, runEventID, options) + ret0, _ := ret[0].(*tfe.RunEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadWithOptions indicates an expected call of ReadWithOptions. +func (mr *MockRunEventsMockRecorder) ReadWithOptions(ctx, runEventID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWithOptions", reflect.TypeOf((*MockRunEvents)(nil).ReadWithOptions), ctx, runEventID, options) +} diff --git a/run_event.go b/run_event.go new file mode 100644 index 000000000..2c22a5ba8 --- /dev/null +++ b/run_event.go @@ -0,0 +1,162 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "net/url" + "time" +) + +// Compile-time proof of interface implementation. +var _ RunEvents = (*runEvents)(nil) + +// RunEvents describes all the run events that the Terraform Enterprise +// API supports. +// +// TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run +type RunEvents interface { + // List all the runs events of the given run. + List(ctx context.Context, runID string, options *RunEventListOptions) (*RunEventList, error) + + // Read a run event by its ID. + Read(ctx context.Context, runEventID string) (*RunEvent, error) + + // ReadWithOptions reads a run event by its ID using the options supplied + ReadWithOptions(ctx context.Context, runEventID string, options *RunEventReadOptions) (*RunEvent, error) +} + +// runEvents implements RunEvents. +type runEvents struct { + client *Client +} + +// RunEventList represents a list of run events. +type RunEventList struct { + // Pagination is not supported by the API + *Pagination + Items []*RunEvent +} + +// Run represents a Terraform Enterprise run. +type RunEvent struct { + ID string `jsonapi:"primary,run-events"` + Action string `jsonapi:"attr,action"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + Description string `jsonapi:"attr,description"` + + // Relations - Note that `target` is not supported yet + Actor *User `jsonapi:"relation,actor"` + Comment *Comment `jsonapi:"relation,comment"` +} + +// RunEventIncludeOpt represents the available options for include query params. +type RunEventIncludeOpt string + +const ( + RunEventComment RunEventIncludeOpt = "comment" + RunEventActor RunEventIncludeOpt = "actor" +) + +// RunListOptions represents the options for listing runs. +type RunEventListOptions struct { + // Optional: A list of relations to include. See available resources: + Include []RunEventIncludeOpt `url:"include,omitempty"` +} + +// RunReadOptions represents the options for reading a run. +type RunEventReadOptions struct { + // Optional: A list of relations to include. See available resources: + Include []RunEventIncludeOpt `url:"include,omitempty"` +} + +// List all the runs of the given workspace. +func (s *runEvents) List(ctx context.Context, runID string, options *RunEventListOptions) (*RunEventList, error) { + if !validStringID(&runID) { + return nil, ErrInvalidRunID + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("runs/%s/run-events", url.QueryEscape(runID)) + + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + rl := &RunEventList{} + err = req.Do(ctx, rl) + if err != nil { + return nil, err + } + + return rl, nil +} + +// Read a run by its ID. +func (s *runEvents) Read(ctx context.Context, runEventID string) (*RunEvent, error) { + return s.ReadWithOptions(ctx, runEventID, nil) +} + +// Read a run by its ID with the given options. +func (s *runEvents) ReadWithOptions(ctx context.Context, runEventID string, options *RunEventReadOptions) (*RunEvent, error) { + if !validStringID(&runEventID) { + return nil, ErrInvalidRunEventID + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("run-events/%s", url.QueryEscape(runEventID)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + r := &RunEvent{} + err = req.Do(ctx, r) + if err != nil { + return nil, err + } + + return r, nil +} + +func (o *RunEventReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateRunEventIncludeParam(o.Include); err != nil { + return err + } + return nil +} + +func (o *RunEventListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + + if err := validateRunEventIncludeParam(o.Include); err != nil { + return err + } + return nil +} + +func validateRunEventIncludeParam(params []RunEventIncludeOpt) error { + for _, p := range params { + switch p { + case RunEventActor, RunEventComment: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/run_event_integration_test.go b/run_event_integration_test.go new file mode 100644 index 000000000..ee17d74c6 --- /dev/null +++ b/run_event_integration_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunEventsList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wTest, _ := createWorkspace(t, client, orgTest) + rTest, _ := createRun(t, client, wTest) + commentText := "Test comment" + _, err := client.Comments.Create(ctx, rTest.ID, CommentCreateOptions{ + Body: commentText, + }) + require.NoError(t, err) + + t.Run("without list options", func(t *testing.T) { + rl, err := client.RunEvents.List(ctx, rTest.ID, nil) + require.NoError(t, err) + + require.NotEmpty(t, rl.Items) + // Find the comment that was added + var commentEvent *RunEvent = nil + for _, event := range rl.Items { + if event.Action == "commented" { + commentEvent = event + } + } + assert.NotNil(t, commentEvent) + // We didn't include any resources so these should be empty + assert.Empty(t, commentEvent.Actor.Username) + assert.Empty(t, commentEvent.Comment.Body) + }) + + t.Run("with all includes", func(t *testing.T) { + rl, err := client.RunEvents.List(ctx, rTest.ID, &RunEventListOptions{ + Include: []RunEventIncludeOpt{RunEventActor, RunEventComment}, + }) + require.NoError(t, err) + + // Find the comment that was added + var commentEvent *RunEvent = nil + for _, event := range rl.Items { + if event.Action == "commented" { + commentEvent = event + } + } + require.NotNil(t, commentEvent) + + // Assert that the include resources are included + require.NotNil(t, commentEvent.Actor) + assert.NotEmpty(t, commentEvent.Actor.Username) + require.NotNil(t, commentEvent.Comment) + assert.Equal(t, commentEvent.Comment.Body, commentText) + }) + + t.Run("without a valid run ID", func(t *testing.T) { + rl, err := client.RunEvents.List(ctx, badIdentifier, nil) + assert.Nil(t, rl) + assert.EqualError(t, err, ErrInvalidRunID.Error()) + }) +} + +func TestRunEventsRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wTest, _ := createWorkspace(t, client, orgTest) + rTest, _ := createRun(t, client, wTest) + commentText := "Test comment" + _, err := client.Comments.Create(ctx, rTest.ID, CommentCreateOptions{ + Body: commentText, + }) + require.NoError(t, err) + + rl, err := client.RunEvents.List(ctx, rTest.ID, nil) + require.NoError(t, err) + // Find the comment that was added + var commentEvent *RunEvent = nil + for _, event := range rl.Items { + if event.Action == "commented" { + commentEvent = event + } + } + assert.NotNil(t, commentEvent) + + t.Run("without read options", func(t *testing.T) { + re, err := client.RunEvents.Read(ctx, commentEvent.ID) + require.NoError(t, err) + + // We didn't include any resources so these should be empty + assert.Empty(t, re.Actor.Username) + assert.Empty(t, re.Comment.Body) + }) + + t.Run("with all includes", func(t *testing.T) { + re, err := client.RunEvents.ReadWithOptions(ctx, commentEvent.ID, &RunEventReadOptions{ + Include: []RunEventIncludeOpt{RunEventActor, RunEventComment}, + }) + require.NoError(t, err) + + // Assert that the include resources are included + require.NotNil(t, re.Actor) + assert.NotEmpty(t, re.Actor.Username) + require.NotNil(t, re.Comment) + assert.Equal(t, re.Comment.Body, commentText) + }) + + t.Run("without a valid run event ID", func(t *testing.T) { + rl, err := client.RunEvents.Read(ctx, badIdentifier) + assert.Nil(t, rl) + assert.EqualError(t, err, ErrInvalidRunEventID.Error()) + }) +} diff --git a/tfe.go b/tfe.go index 9f74a01ca..df4fd30ac 100644 --- a/tfe.go +++ b/tfe.go @@ -153,6 +153,7 @@ type Client struct { RegistryProviderPlatforms RegistryProviderPlatforms RegistryProviderVersions RegistryProviderVersions Runs Runs + RunEvents RunEvents RunTasks RunTasks RunTriggers RunTriggers SSHKeys SSHKeys @@ -404,6 +405,7 @@ func NewClient(cfg *Config) (*Client, error) { client.RegistryProviders = ®istryProviders{client: client} client.RegistryProviderVersions = ®istryProviderVersions{client: client} client.Runs = &runs{client: client} + client.RunEvents = &runEvents{client: client} client.RunTasks = &runTasks{client: client} client.RunTriggers = &runTriggers{client: client} client.SSHKeys = &sshKeys{client: client} From d7628cd130062580d194266939959e4d96b5626d Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Tue, 11 Apr 2023 13:27:25 -0600 Subject: [PATCH 2/3] Fix RunEvents godocs --- run_event.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/run_event.go b/run_event.go index 2c22a5ba8..27da1073f 100644 --- a/run_event.go +++ b/run_event.go @@ -40,7 +40,7 @@ type RunEventList struct { Items []*RunEvent } -// Run represents a Terraform Enterprise run. +// RunEvent represents a Terraform Enterprise run event. type RunEvent struct { ID string `jsonapi:"primary,run-events"` Action string `jsonapi:"attr,action"` @@ -60,7 +60,7 @@ const ( RunEventActor RunEventIncludeOpt = "actor" ) -// RunListOptions represents the options for listing runs. +// RunEventListOptions represents the options for listing run events. type RunEventListOptions struct { // Optional: A list of relations to include. See available resources: Include []RunEventIncludeOpt `url:"include,omitempty"` @@ -72,7 +72,7 @@ type RunEventReadOptions struct { Include []RunEventIncludeOpt `url:"include,omitempty"` } -// List all the runs of the given workspace. +// List all the run events of the given run. func (s *runEvents) List(ctx context.Context, runID string, options *RunEventListOptions) (*RunEventList, error) { if !validStringID(&runID) { return nil, ErrInvalidRunID @@ -102,7 +102,7 @@ func (s *runEvents) Read(ctx context.Context, runEventID string) (*RunEvent, err return s.ReadWithOptions(ctx, runEventID, nil) } -// Read a run by its ID with the given options. +// ReadWithOptions reads a run by its ID with the given options. func (s *runEvents) ReadWithOptions(ctx context.Context, runEventID string, options *RunEventReadOptions) (*RunEvent, error) { if !validStringID(&runEventID) { return nil, ErrInvalidRunEventID From e1487c52576c32d759460af10859ce1dcf13628d Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Tue, 11 Apr 2023 13:33:15 -0600 Subject: [PATCH 3/3] Fix RunEvents godocs --- run_event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_event.go b/run_event.go index 27da1073f..0162d8297 100644 --- a/run_event.go +++ b/run_event.go @@ -66,7 +66,7 @@ type RunEventListOptions struct { Include []RunEventIncludeOpt `url:"include,omitempty"` } -// RunReadOptions represents the options for reading a run. +// RunEventReadOptions represents the options for reading a run event. type RunEventReadOptions struct { // Optional: A list of relations to include. See available resources: Include []RunEventIncludeOpt `url:"include,omitempty"`