Skip to content

Commit

Permalink
Add agent token support (#169)
Browse files Browse the repository at this point in the history
Feature: add agent token support

Co-authored-by: Chris Arcand <[email protected]>
  • Loading branch information
jgrumboe and chrisarcand authored Jan 20, 2021
1 parent 46f8be3 commit f3a4dd1
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 0 deletions.
145 changes: 145 additions & 0 deletions agent_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package tfe

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

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

// AgentTokens describes all the agent token related methods that the
// Terraform Cloud API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/cloud/api/agent-tokens.html
type AgentTokens interface {
// List all the agent tokens of the given agent pool.
List(ctx context.Context, agentPoolID string) (*AgentTokenList, error)

// Generate a new agent token with the given options.
Generate(ctx context.Context, agentPoolID string, options AgentTokenGenerateOptions) (*AgentToken, error)

// Read an agent token by its ID.
Read(ctx context.Context, agentTokenID string) (*AgentToken, error)

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

// agentTokens implements AgentTokens.
type agentTokens struct {
client *Client
}

// AgentTokenList represents a list of agent tokens.
type AgentTokenList struct {
*Pagination
Items []*AgentToken
}

// AgentToken represents a Terraform Cloud agent token.
type AgentToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
}

// List all the agent tokens of the given agent pool.
func (s *agentTokens) List(ctx context.Context, agentPoolID string) (*AgentTokenList, error) {
if !validStringID(&agentPoolID) {
return nil, errors.New("invalid value for agent pool ID")
}

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

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

return tokenList, nil
}

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

// Description of the token
Description *string `jsonapi:"attr,description"`
}

// Generate a new agent token with the given options.
func (s *agentTokens) Generate(ctx context.Context, agentPoolID string, options AgentTokenGenerateOptions) (*AgentToken, error) {
if !validStringID(&agentPoolID) {
return nil, errors.New("invalid value for agent pool ID")
}

if !validString(options.Description) {
return nil, errors.New("agent token description can't be blank")
}

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

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

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

return at, err
}

// Read an agent token by its ID.
func (s *agentTokens) Read(ctx context.Context, agentTokenID string) (*AgentToken, error) {
if !validStringID(&agentTokenID) {
return nil, errors.New("invalid value for agent token ID")
}

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

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

return at, err
}

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

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

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

import (
"context"
"testing"

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

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

apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()

agentToken1, agentToken1Cleanup := createAgentToken(t, client, apTest)
defer agentToken1Cleanup()
_, agentToken2Cleanup := createAgentToken(t, client, apTest)
defer agentToken2Cleanup()

t.Run("with no list options", func(t *testing.T) {
tokenlist, err := client.AgentTokens.List(ctx, apTest.ID)
require.NoError(t, err)
var found bool
for _, j := range tokenlist.Items {
if j.ID == agentToken1.ID {
found = true
break
}
}
if !found {
t.Fatalf("agent token (%s) not found in token list", agentToken1.ID)
}

assert.Equal(t, 1, tokenlist.CurrentPage)
assert.Equal(t, 2, tokenlist.TotalCount)
})

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

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

apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()

t.Run("with valid description", func(t *testing.T) {
token, err := client.AgentTokens.Generate(ctx, apTest.ID, AgentTokenGenerateOptions{
Description: String(randomString(t)),
})
require.NoError(t, err)
require.NotEmpty(t, token.Token)
})

t.Run("without valid description", func(t *testing.T) {
at, err := client.AgentTokens.Generate(ctx, badIdentifier, AgentTokenGenerateOptions{})
assert.Nil(t, at)
assert.EqualError(t, err, "invalid value for agent pool ID")
})

t.Run("without valid agent pool ID", func(t *testing.T) {
at, err := client.AgentTokens.Generate(ctx, badIdentifier, AgentTokenGenerateOptions{
Description: String(randomString(t)),
})
assert.Nil(t, at)
assert.EqualError(t, err, "invalid value for agent pool ID")
})
}
func TestAgentTokensRead(t *testing.T) {
client := testClient(t)
ctx := context.Background()

apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()

token, tokenTestCleanup := createAgentToken(t, client, apTest)
defer tokenTestCleanup()

t.Run("read token with valid token ID", func(t *testing.T) {
at, err := client.AgentTokens.Read(ctx, token.ID)
assert.NoError(t, err)
// The initial API call to create a token will return a value in the token
// object. Empty that out for comparison
token.Token = ""
assert.Equal(t, token, at)
})

t.Run("read token without valid token ID", func(t *testing.T) {
_, err := client.AgentTokens.Read(ctx, badIdentifier)
assert.EqualError(t, err, "invalid value for agent token ID")
})
}

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

apTest, apTestCleanup := createAgentPool(t, client, nil)
defer apTestCleanup()

token, _ := createAgentToken(t, client, apTest)

t.Run("with valid token ID", func(t *testing.T) {
err := client.AgentTokens.Delete(ctx, token.ID)
assert.NoError(t, err)
})

t.Run("without valid token ID", func(t *testing.T) {
err := client.AgentTokens.Delete(ctx, badIdentifier)
assert.EqualError(t, err, "invalid value for agent token ID")
})
}
28 changes: 28 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@ func createAgentPool(t *testing.T, client *Client, org *Organization) (*AgentPoo
}
}

func createAgentToken(t *testing.T, client *Client, ap *AgentPool) (*AgentToken, func()) {
var apCleanup func()

if ap == nil {
ap, apCleanup = createAgentPool(t, client, nil)
}

ctx := context.Background()
at, err := client.AgentTokens.Generate(ctx, ap.ID, AgentTokenGenerateOptions{
Description: String(randomString(t)),
})
if err != nil {
t.Fatal(err)
}

return at, func() {
if err := client.AgentTokens.Delete(ctx, at.ID); err != nil {
t.Errorf("Error destroying agent token! WARNING: Dangling resources\n"+
"may exist! The full error is shown below.\n\n"+
"AgentToken: %s\nError: %s", at.ID, err)
}

if apCleanup != nil {
apCleanup()
}
}
}

func createConfigurationVersion(t *testing.T, client *Client, w *Workspace) (*ConfigurationVersion, func()) {
var wCleanup func()

Expand Down
2 changes: 2 additions & 0 deletions tfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ type Client struct {
remoteAPIVersion string

AgentPools AgentPools
AgentTokens AgentTokens
Applies Applies
ConfigurationVersions ConfigurationVersions
CostEstimates CostEstimates
Expand Down Expand Up @@ -221,6 +222,7 @@ func NewClient(cfg *Config) (*Client, error) {

// Create the services.
client.AgentPools = &agentPools{client: client}
client.AgentTokens = &agentTokens{client: client}
client.Applies = &applies{client: client}
client.ConfigurationVersions = &configurationVersions{client: client}
client.CostEstimates = &costEstimates{client: client}
Expand Down

0 comments on commit f3a4dd1

Please sign in to comment.