Skip to content

Commit

Permalink
Merge pull request #153 from hashicorp/tfc-agents
Browse files Browse the repository at this point in the history
Add support for TFC Agent Pools
  • Loading branch information
chrisarcand authored Nov 19, 2020
2 parents e09c2fe + 2afbcc8 commit 87d99d2
Show file tree
Hide file tree
Showing 6 changed files with 536 additions and 3 deletions.
205 changes: 205 additions & 0 deletions agent_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package tfe

import (
"context"
"errors"
"fmt"
"net/url"
)

// Compile-time proof of interface implementation.
var _ AgentPools = (*agentPools)(nil)

// AgentPools describes all the agent pool related methods that the Terraform
// Cloud API supports. Note that agents are not available in Terraform Enterprise.
//
// TFE API docs: https://www.terraform.io/docs/cloud/api/agents.html
type AgentPools interface {
// List all the agent pools of the given organization.
List(ctx context.Context, organization string, options AgentPoolListOptions) (*AgentPoolList, error)

// Create a new agent pool with the given options.
Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error)

// Read a agent pool by its ID.
Read(ctx context.Context, agentPoolID string) (*AgentPool, error)

// Update an agent pool by its ID.
Update(ctx context.Context, agentPool string, options AgentPoolUpdateOptions) (*AgentPool, error)

// Delete an agent pool by its ID.
Delete(ctx context.Context, agentPoolID string) error
}

// agentPools implements AgentPools.
type agentPools struct {
client *Client
}

// AgentPoolList represents a list of agent pools.
type AgentPoolList struct {
*Pagination
Items []*AgentPool
}

// AgentPool represents a Terraform Cloud agent pool.
type AgentPool struct {
ID string `jsonapi:"primary,agent-pools"`
Name string `jsonapi:"attr,name"`

// Relations
Organization *Organization `jsonapi:"relation,organization"`
}

// AgentPoolListOptions represents the options for listing agent pools.
type AgentPoolListOptions struct {
ListOptions
}

// List all the agent pools of the given organization.
func (s *agentPools) List(ctx context.Context, organization string, options AgentPoolListOptions) (*AgentPoolList, error) {
if !validStringID(&organization) {
return nil, errors.New("invalid value for organization")
}

u := fmt.Sprintf("organizations/%s/agent-pools", url.QueryEscape(organization))
req, err := s.client.newRequest("GET", u, &options)
if err != nil {
return nil, err
}

poolList := &AgentPoolList{}
err = s.client.do(ctx, req, poolList)
if err != nil {
return nil, err
}

return poolList, nil
}

// AgentPoolCreateOptions represents the options for creating an agent pool.
type AgentPoolCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,agent-pools"`

// A name to identify the agent pool.
Name *string `jsonapi:"attr,name"`
}

func (o AgentPoolCreateOptions) valid() error {
if !validString(o.Name) {
return errors.New("name is required")
}
if !validStringID(o.Name) {
return errors.New("invalid value for name")
}
return nil
}

// Create a new agent pool with the given options.
func (s *agentPools) Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error) {
if !validStringID(&organization) {
return nil, errors.New("invalid value for organization")
}

if err := options.valid(); err != nil {
return nil, err
}

// Make sure we don't send a user provided ID.
options.ID = ""

u := fmt.Sprintf("organizations/%s/agent-pools", url.QueryEscape(organization))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}

pool := &AgentPool{}
err = s.client.do(ctx, req, pool)
if err != nil {
return nil, err
}

return pool, nil
}

// Read a single agent pool by its ID.
func (s *agentPools) Read(ctx context.Context, agentpoolID string) (*AgentPool, error) {
if !validStringID(&agentpoolID) {
return nil, errors.New("invalid value for agent pool ID")
}

u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentpoolID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

pool := &AgentPool{}
err = s.client.do(ctx, req, pool)
if err != nil {
return nil, err
}

return pool, nil
}

// AgentPoolUpdateOptions represents the options for updating an agent pool.
type AgentPoolUpdateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,agent-pools"`

// A new name to identify the agent pool.
Name *string `jsonapi:"attr,name"`
}

func (o AgentPoolUpdateOptions) valid() error {
if o.Name != nil && !validStringID(o.Name) {
return errors.New("invalid value for name")
}
return nil
}

// Update an agent pool by its ID.
func (s *agentPools) Update(ctx context.Context, agentPoolID string, options AgentPoolUpdateOptions) (*AgentPool, error) {
if !validStringID(&agentPoolID) {
return nil, errors.New("invalid value for agent pool ID")
}

if err := options.valid(); err != nil {
return nil, err
}

// Make sure we don't send a user provided ID.
options.ID = ""

u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}

k := &AgentPool{}
err = s.client.do(ctx, req, k)
if err != nil {
return nil, err
}

return k, nil
}

// Delete an agent pool by its ID.
func (s *agentPools) Delete(ctx context.Context, agentPoolID string) error {
if !validStringID(&agentPoolID) {
return errors.New("invalid value for agent pool ID")
}

u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}

return s.client.do(ctx, req, nil)
}
189 changes: 189 additions & 0 deletions agent_pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package tfe

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAgentPoolsList(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

agentPool, _ := createAgentPool(t, client, orgTest)

t.Run("without list options", func(t *testing.T) {
pools, err := client.AgentPools.List(ctx, orgTest.Name, AgentPoolListOptions{})
require.NoError(t, err)
assert.Contains(t, pools.Items, agentPool)

assert.Equal(t, 1, pools.CurrentPage)
assert.Equal(t, 1, pools.TotalCount)
})

t.Run("with list options", func(t *testing.T) {
// Request a page number which is out of range. The result should
// be successful, but return no results if the paging options are
// properly passed along.
pools, err := client.AgentPools.List(ctx, orgTest.Name, AgentPoolListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Empty(t, pools.Items)
assert.Equal(t, 999, pools.CurrentPage)
assert.Equal(t, 1, pools.TotalCount)
})

t.Run("without a valid organization", func(t *testing.T) {
pools, err := client.AgentPools.List(ctx, badIdentifier, AgentPoolListOptions{})
assert.Nil(t, pools)
assert.EqualError(t, err, "invalid value for organization")
})
}

func TestAgentPoolsCreate(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

t.Run("with valid options", func(t *testing.T) {
options := AgentPoolCreateOptions{
Name: String("cool-pool"),
}

pool, err := client.AgentPools.Create(ctx, orgTest.Name, options)
require.NoError(t, err)

// Get a refreshed view from the API.
refreshed, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)

for _, item := range []*AgentPool{
pool,
refreshed,
} {
assert.NotEmpty(t, item.ID)
}
})

t.Run("when options is missing name", func(t *testing.T) {
k, err := client.AgentPools.Create(ctx, "foo", AgentPoolCreateOptions{})
assert.Nil(t, k)
assert.EqualError(t, err, "name is required")
})

t.Run("with an invalid organization", func(t *testing.T) {
pool, err := client.AgentPools.Create(ctx, badIdentifier, AgentPoolCreateOptions{
Name: String("cool-pool"),
})
assert.Nil(t, pool)
assert.EqualError(t, err, "invalid value for organization")
})
}

func TestAgentPoolsRead(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

pool, _ := createAgentPool(t, client, orgTest)

t.Run("when the agent pool exists", func(t *testing.T) {
k, err := client.AgentPools.Read(ctx, pool.ID)
require.NoError(t, err)
assert.Equal(t, pool, k)
})

t.Run("when the agent pool does not exist", func(t *testing.T) {
k, err := client.AgentPools.Read(ctx, "nonexisting")
assert.Nil(t, k)
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("without a valid agent pool ID", func(t *testing.T) {
k, err := client.AgentPools.Read(ctx, badIdentifier)
assert.Nil(t, k)
assert.EqualError(t, err, "invalid value for agent pool ID")
})
}

func TestAgentPoolsUpdate(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

t.Run("with valid options", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
defer kTestCleanup()

kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
Name: String(randomString(t)),
})
require.NoError(t, err)

assert.Equal(t, kBefore.ID, kAfter.ID)
assert.NotEqual(t, kBefore.Name, kAfter.Name)
})

t.Run("when updating the name", func(t *testing.T) {
kBefore, kTestCleanup := createAgentPool(t, client, orgTest)
defer kTestCleanup()

kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{
Name: String("updated-key-name"),
})
require.NoError(t, err)

assert.Equal(t, kBefore.ID, kAfter.ID)
assert.Equal(t, "updated-key-name", kAfter.Name)
})

t.Run("without a valid agent pool ID", func(t *testing.T) {
w, err := client.AgentPools.Update(ctx, badIdentifier, AgentPoolUpdateOptions{})
assert.Nil(t, w)
assert.EqualError(t, err, "invalid value for agent pool ID")
})
}

func TestAgentPoolsDelete(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

kTest, _ := createAgentPool(t, client, orgTest)

t.Run("with valid options", func(t *testing.T) {
err := client.AgentPools.Delete(ctx, kTest.ID)
require.NoError(t, err)

// Try loading the agent pool - it should fail.
_, err = client.AgentPools.Read(ctx, kTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("when the agent pool does not exist", func(t *testing.T) {
err := client.AgentPools.Delete(ctx, kTest.ID)
assert.Equal(t, err, ErrResourceNotFound)
})

t.Run("when the agent pool ID is invalid", func(t *testing.T) {
err := client.AgentPools.Delete(ctx, badIdentifier)
assert.EqualError(t, err, "invalid value for agent pool ID")
})
}
Loading

0 comments on commit 87d99d2

Please sign in to comment.