Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for querying run events #680

Merged
merged 4 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

giphy

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