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 agent data source #456

Merged
merged 23 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cd5c837
Add new agent data source to client
laurenolivia Jul 27, 2022
30d48a6
merge conflicts
laurenolivia Jul 27, 2022
40387c3
resolve errors.go
laurenolivia Jul 27, 2022
b2451da
Merge remote-tracking branch 'origin' into laurenolivia/add-agent-dat…
laurenolivia Aug 3, 2022
2bc473b
merge conflicts, re-add createAgent fn
laurenolivia Aug 3, 2022
c9cd163
cleaning up log statements, shorten helper fn sig
laurenolivia Aug 3, 2022
17618f1
remove options from interface
laurenolivia Aug 4, 2022
ce05456
refactor
laurenolivia Aug 5, 2022
7d6ecc7
Merge remote-tracking branch 'origin' into laurenolivia/add-agent-dat…
laurenolivia Aug 5, 2022
b9674cc
small fixes
laurenolivia Aug 5, 2022
055069d
refactor helper fn, integration tests
laurenolivia Aug 9, 2022
587fdf0
Merge remote-tracking branch 'origin' into laurenolivia/add-agent-dat…
laurenolivia Aug 10, 2022
5bd3c24
replace docker with biniary
laurenolivia Aug 10, 2022
53518dc
add
laurenolivia Aug 10, 2022
94bc1e4
create agent pool within create agent fn, return pool
laurenolivia Aug 10, 2022
c16d315
add mocks
laurenolivia Aug 10, 2022
f1cc0db
create skipIfNotRuntime fn
laurenolivia Aug 10, 2022
f81e98b
remove extra line
laurenolivia Aug 10, 2022
9a7d547
move upgradeOrg down one line
laurenolivia Aug 11, 2022
d6a6734
rename skipIf fn
laurenolivia Aug 11, 2022
b98f0f0
Merge remote-tracking branch 'origin' into laurenolivia/add-agent-dat…
laurenolivia Aug 11, 2022
1021667
remove a var not in main
laurenolivia Aug 11, 2022
a493b3d
rename to LastPingSince
laurenolivia Aug 11, 2022
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
6 changes: 5 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ jobs:
steps:
- checkout

- setup_remote_docker:
version: 20.10.14
docker_layer_caching: true

- run:
name: Make test results directory
command: mkdir -p $TEST_RESULTS_DIR
Expand All @@ -36,4 +40,4 @@ workflows:
my-workflow:
jobs:
- run-tests:
context: core-team-access
context: core-team-access
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ For complete usage of the API client, see the [full package docs](https://pkg.go
This API client covers most of the existing Terraform Cloud API calls and is updated regularly to add new or missing endpoints.

- [x] Account
- [x] Agents
- [x] Agent Pools
- [x] Agent Tokens
- [x] Applies
Expand Down
155 changes: 155 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package tfe

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

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

// Agents describes all the agent-related methods that the
// Terraform Cloud API supports.
// TFE API docs: https://www.terraform.io/docs/cloud/api/agents.html
type Agents interface {
// Read an agent by its ID.
Read(ctx context.Context, agentID string) (*Agent, error)

// Read an agent by its ID with the given options.
ReadWithOptions(ctx context.Context, agentID string, options *AgentReadOptions) (*Agent, error)

// List all the agents of the given pool.
List(ctx context.Context, agentPoolID string, options *AgentListOptions) (*AgentList, error)
}

// agents implements Agents.
type agents struct {
client *Client
}

// AgentList represents a list of agents.
type AgentList struct {
*Pagination
Items []*Agent
}

// Agent represents a Terraform Cloud agent.
type Agent struct {
ID string `jsonapi:"primary,agents"`
Name string `jsonapi:"attr,name"`
IP string `jsonapi:"attr,ip-address"`

// Relations
Organization *Organization `jsonapi:"relation,organization"`
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
}

// A list of relations to include
// https://www.terraform.io/cloud-docs/api-docs/agents#available-related-resources
type AgentIncludeOpt string

const (
AgentWorkspaces AgentIncludeOpt = "workspaces"
)

// AgentReadOptions represents the options for reading an agent.
type AgentReadOptions struct {
Include []AgentIncludeOpt `url:"include,omitempty"`
}

// AgentListOptions represents the options for listing agents.
type AgentListOptions struct {
ListOptions
// Optional: A list of relations to include. See available resources
// https://www.terraform.io/cloud-docs/api-docs/agents#available-related-resources
Include []AgentIncludeOpt `url:"include,omitempty"`
}

// Read a single agent by its ID
func (s *agents) Read(ctx context.Context, agentID string) (*Agent, error) {
return s.ReadWithOptions(ctx, agentID, nil)
}

// Read a single agent by its ID with options.
func (s *agents) ReadWithOptions(ctx context.Context, agentID string, options *AgentReadOptions) (*Agent, error) {
if !validStringID(&agentID) {
return nil, ErrInvalidAgentID //undeclared var name
}
if err := options.valid(); err != nil {
return nil, err
}

u := fmt.Sprintf("agents/%s", url.QueryEscape(agentID))
req, err := s.client.NewRequest("GET", u, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should pass options here instead of nil.

Copy link
Member

Choose a reason for hiding this comment

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

Possibly another sign that this method isn't needed! 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sebasslash 👍
@nfagerlund Can you elaborate on why it's a sign that the method, possibly, isn't needed❔

if err != nil {
return nil, err
}

agent := &Agent{}
err = req.Do(ctx, agent)
if err != nil {
return nil, err
}

return agent, nil //cannot use agent as *Agent value in return statement
}

// List all the agents of the given organization.
func (s *agents) List(ctx context.Context, agentPoolID string, options *AgentListOptions) (*AgentList, error) {
if !validStringID(&agentPoolID) {
return nil, ErrInvalidOrg
}
if err := options.valid(); err != nil {
return nil, err
}

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

agentList := &AgentList{}
err = req.Do(ctx, agentList)
if err != nil {
return nil, err
}

return agentList, nil
}

func (o *AgentReadOptions) valid() error {
if o == nil {
return nil // nothing to validate
}
if err := validateAgentIncludeParams(o.Include); err != nil {
return err
}

return nil
}

func (o *AgentListOptions) valid() error {
if o == nil {
return nil // nothing to validate
}
if err := validateAgentIncludeParams(o.Include); err != nil {
return err
}

return nil
}

func validateAgentIncludeParams(params []AgentIncludeOpt) error {
for _, p := range params {
switch p {
case AgentWorkspaces:
// do nothing
default:
return ErrInvalidIncludeValue
}
}

return nil
}
64 changes: 64 additions & 0 deletions agent_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build integration
// +build integration

package tfe

import (
"context"
"testing"

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

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

agent, _, agentCleanup := createAgent(t, client, nil, nil, nil)
defer agentCleanup()
t.Log("log agent: ", agent)
// t.Log("log pool: ", pool)

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

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

t.Run("without a valid agent ID", func(t *testing.T) {
k, err := client.Agents.Read(ctx, badIdentifier)
assert.Nil(t, k)
assert.EqualError(t, err, ErrInvalidAgentID.Error())
})
}

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

agent, agentPool, agentCleanup := createAgent(t, client, nil, nil, nil)
defer agentCleanup()
t.Log("log agent: ", agent)
t.Log("log agent pool: ", agentPool)

t.Run("expect an agent to exist", func(t *testing.T) {
agent, err := client.Agents.List(ctx, agentPool.ID, nil)

require.NoError(t, err)
require.NotEmpty(t, agent.Items)
assert.NotEmpty(t, agent.Items[0].ID)
})

t.Run("without a valid agent pool ID", func(t *testing.T) {
agents, err := client.Agents.List(ctx, badIdentifier, nil)
assert.Nil(t, agents)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})
}
4 changes: 2 additions & 2 deletions agent_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ type AgentPools interface {
// 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 an agent pool by its ID.
Read(ctx context.Context, agentPoolID string) (*AgentPool, error)

// Read a agent pool by its ID with the given options.
// Read an agent pool by its ID with the given options.
ReadWithOptions(ctx context.Context, agentPoolID string, options *AgentPoolReadOptions) (*AgentPool, error)

// Update an agent pool by its ID.
Expand Down
2 changes: 2 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ var (

ErrInvalidArch = errors.New("invalid value for arch")

ErrInvalidAgentID = errors.New("invalid value for Agent ID")

ErrInvalidRegistryName = errors.New(`invalid value for registry-name. It must be either "private" or "public"`)
)

Expand Down
1 change: 1 addition & 0 deletions generate_mocks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ mockgen -source=variable.go -destination=mocks/variable_mocks.go -package=mocks
mockgen -source=variable_set.go -destination=mocks/variable_set_mocks.go -package=mocks
mockgen -source=workspace.go -destination=mocks/workspace_mocks.go -package=mocks
mockgen -source=workspace_run_task.go -destination=mocks/workspace_run_tasks.go -package=mocks
mockgen -source=agent.go -destination=mocks/agents.go -package=mocks
80 changes: 80 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"math/rand"
"net/url"
"os"
"os/exec"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -88,6 +89,85 @@ func fetchTestAccountDetails(t *testing.T, client *Client) *TestAccountDetails {
return _testAccountDetails
}

func createAgent(t *testing.T, client *Client, org *Organization, agentPool *AgentPool, agentPoolToken *AgentToken) (*Agent, *AgentPool, func()) {
var orgCleanup func()
var agentPoolCleanup func()
var agentPoolTokenCleanup func()
var agent *Agent

if org == nil {
org, orgCleanup = createOrganization(t, client)
}

upgradeOrganizationSubscription(t, client, org)

if agentPool == nil {
agentPool, agentPoolCleanup = createAgentPool(t, client, org)
t.Log("create, log agentPool: ", agentPool)
}

if agentPoolToken == nil {
agentPoolToken, agentPoolTokenCleanup = createAgentToken(t, client, agentPool)
}

ctx := context.Background()
cmd := exec.Command("docker",
"run", "-d",
"--env", "TFC_AGENT_TOKEN="+agentPoolToken.Token,
"--env", "TFC_AGENT_NAME="+"this-is-a-test-agent",
"--env", "TFC_ADDRESS="+DefaultConfig().Address,
"docker.mirror.hashicorp.services/hashicorp/tfc-agent:latest")

go func() {
output, err := cmd.CombinedOutput()
if err != nil {
t.Logf("Could not run container: %s", err)
}

t.Log("Logging container output: ", (string)(output))
}()

defer func() {
t.Log("Cleaning up agent docker container: ")
cmd := exec.Command("docker", "rm", "-f")
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, does this actually work as expected? It looks like it's not passing the required container argument to docker rm. When I run this on my local machine it just fails with a complaint.

_ = cmd.Run()
}()

i, err := retry(func() (interface{}, error) {

agentList, err := client.Agents.List(ctx, agentPool.ID, nil)
if err != nil {
return nil, err
}

if agentList != nil && len(agentList.Items) > 0 {
return agentList.Items[0], nil
}
return nil, errors.New("No agent found.")
})

if err != nil {
t.Fatalf("Could not return an agent %s", err)
}

agent = i.(*Agent)
t.Log("log agent, after type assertion: ", agent)

return agent, agentPool, func() {
if agentPoolTokenCleanup != nil {
agentPoolTokenCleanup()
}

if agentPoolCleanup != nil {
agentPoolCleanup()
}

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

func createAgentPool(t *testing.T, client *Client, org *Organization) (*AgentPool, func()) {
var orgCleanup func()

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

Admin Admin
Agents Agents
AgentPools AgentPools
AgentTokens AgentTokens
Applies Applies
Expand Down Expand Up @@ -351,6 +352,7 @@ func NewClient(cfg *Config) (*Client, error) {
}

// Create the services.
client.Agents = &agents{client: client}
client.AgentPools = &agentPools{client: client}
client.AgentTokens = &agentTokens{client: client}
client.Applies = &applies{client: client}
Expand Down