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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion .circleci/config.yml
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
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
@@ -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)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

// 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"`
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

// Relations
Organization *Organization `jsonapi:"relation,organization"`
Workspaces []*Workspace `jsonapi:"relation,workspaces"`
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
// 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"`
}
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

// 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
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
}
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
Contributor

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
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 {
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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()
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
t.Log("log agent: ", agent)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
// t.Log("log pool: ", pool)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

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)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
defer agentCleanup()
t.Log("log agent: ", agent)
t.Log("log agent pool: ", agentPool)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

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
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
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
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
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()) {
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
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)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
}

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() {
brandonc marked this conversation as resolved.
Show resolved Hide resolved
output, err := cmd.CombinedOutput()
if err != nil {
t.Logf("Could not run container: %s", err)
}

t.Log("Logging container output: ", (string)(output))
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
}()

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

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()
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
}()

i, err := retry(func() (interface{}, error) {
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

agentList, err := client.Agents.List(ctx, agentPool.ID, nil)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved
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)
laurenolivia marked this conversation as resolved.
Show resolved Hide resolved

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
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