-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #153 from hashicorp/tfc-agents
Add support for TFC Agent Pools
- Loading branch information
Showing
6 changed files
with
536 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) | ||
} |
Oops, something went wrong.