Skip to content

Commit

Permalink
Merge pull request #680 from hashicorp/gs/add-run-events
Browse files Browse the repository at this point in the history
Add support for querying run events
  • Loading branch information
brandonc authored Apr 11, 2023
2 parents 2a1107b + e1487c5 commit 3429064
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions generate_mocks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 81 additions & 0 deletions mocks/run_events_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 162 additions & 0 deletions run_event.go
Original file line number Diff line number Diff line change
@@ -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
}

// RunEvent represents a Terraform Enterprise run event.
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"
)

// 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"`
}

// 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"`
}

// 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
}
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)
}

// 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
}
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
}
129 changes: 129 additions & 0 deletions run_event_integration_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
Loading

0 comments on commit 3429064

Please sign in to comment.