Skip to content

Commit

Permalink
Merge pull request #920 from hashicorp/TF-17012-stacks-support-in-go-tfe
Browse files Browse the repository at this point in the history
Stacks support (Beta)
  • Loading branch information
brandonc authored Jun 27, 2024
2 parents f6f1791 + c4fda27 commit f18ee8e
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# UNRELEASED

## Enhancements

* Adds BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users. [#920](https://github.com/hashicorp/go-tfe/pull/920)

# v1.57.0

## Enhancements
Expand Down
209 changes: 209 additions & 0 deletions stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package tfe

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

// Stacks describes all the stacks-related methods that the HCP Terraform API supports.
// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the
// release notes.
type Stacks interface {
// List returns a list of stacks, optionally filtered by project.
List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error)

// Read returns a stack by its ID.
Read(ctx context.Context, stackID string) (*Stack, error)

// Create creates a new stack.
Create(ctx context.Context, options StackCreateOptions) (*Stack, error)

// Update updates a stack.
Update(ctx context.Context, stackID string, options StackUpdateOptions) (*Stack, error)

// Delete deletes a stack.
Delete(ctx context.Context, stackID string) error
}

// stacks implements Stacks.
type stacks struct {
client *Client
}

var _ Stacks = &stacks{}

// StackSortColumn represents a string that can be used to sort items when using
// the List method.
type StackSortColumn string

const (
// StackSortByName sorts by the name attribute.
StackSortByName StackSortColumn = "name"

// StackSortByUpdatedAt sorts by the updated-at attribute.
StackSortByUpdatedAt StackSortColumn = "updated-at"

// StackSortByNameDesc sorts by the name attribute in descending order.
StackSortByNameDesc StackSortColumn = "-name"

// StackSortByUpdatedAtDesc sorts by the updated-at attribute in descending order.
StackSortByUpdatedAtDesc StackSortColumn = "-updated-at"
)

// StackList represents a list of stacks.
type StackList struct {
*Pagination
Items []*Stack
}

// StackVCSRepo represents the version control system repository for a stack.
type StackVCSRepo struct {
Identifier string `jsonapi:"attr,identifier"`
Branch string `jsonapi:"attr,branch"`
GHAInstallationID string `jsonapi:"attr,github-app-installation-id"`
OAuthTokenID string `jsonapi:"attr,oauth-token-id"`
}

// Stack represents a stack.
type Stack struct {
ID string `jsonapi:"primary,stacks"`
Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"`
DeploymentNames []string `jsonapi:"attr,deployment-names"`
VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo"`
ErrorsCount int `jsonapi:"attr,errors-count"`
WarningsCount int `jsonapi:"attr,warnings-count"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`

// Relationships
Project *Project `jsonapi:"relation,project"`
}

// StackListOptions represents the options for listing stacks.
type StackListOptions struct {
ListOptions
ProjectID string `url:"filter[project[id]],omitempty"`
Sort StackSortColumn `url:"sort,omitempty"`
SearchByName string `url:"search[name],omitempty"`
}

// StackCreateOptions represents the options for creating a stack. The project
// relation is required.
type StackCreateOptions struct {
Type string `jsonapi:"primary,stacks"`
Name string `jsonapi:"attr,name"`
Description *string `jsonapi:"attr,description,omitempty"`
VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo"`
Project *Project `jsonapi:"relation,project"`
}

// StackUpdateOptions represents the options for updating a stack.
type StackUpdateOptions struct {
Name *string `jsonapi:"attr,name,omitempty"`
Description *string `jsonapi:"attr,description,omitempty"`
VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo,omitempty"`
}

func (s stacks) List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error) {
if err := options.valid(); err != nil {
return nil, err
}

req, err := s.client.NewRequest("GET", fmt.Sprintf("organizations/%s/stacks", organization), options)
if err != nil {
return nil, err
}

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

return sl, nil
}

func (s stacks) Read(ctx context.Context, stackID string) (*Stack, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), nil)
if err != nil {
return nil, err
}

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

return stack, nil
}

func (s stacks) Create(ctx context.Context, options StackCreateOptions) (*Stack, error) {
if err := options.valid(); err != nil {
return nil, err
}

req, err := s.client.NewRequest("POST", "stacks", &options)
if err != nil {
return nil, err
}

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

return stack, nil
}

func (s stacks) Update(ctx context.Context, stackID string, options StackUpdateOptions) (*Stack, error) {
req, err := s.client.NewRequest("PATCH", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), &options)
if err != nil {
return nil, err
}

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

return stack, nil
}

func (s stacks) Delete(ctx context.Context, stackID string) error {
req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/delete", url.PathEscape(stackID)), nil)
if err != nil {
return err
}

return req.Do(ctx, nil)
}

func (s *StackListOptions) valid() error {
return nil
}

func (s StackCreateOptions) valid() error {
if s.Name == "" {
return ErrRequiredName
}

if s.Project.ID == "" {
return ErrRequiredProject
}

return s.VCSRepo.valid()
}

func (s StackVCSRepo) valid() error {
if s.Identifier == "" {
return ErrRequiredVCSRepo
}

return nil
}
172 changes: 172 additions & 0 deletions stack_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package tfe

import (
"context"
"testing"

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

func TestStackCreateAndList(t *testing.T) {
skipUnlessBeta(t)

client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)

project2, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{
Name: "test-project-2",
})
require.NoError(t, err)

oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)

stack1, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "aa-test-stack",
VCSRepo: &StackVCSRepo{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})

require.NoError(t, err)
require.NotNil(t, stack1)

stack2, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "zz-test-stack",
VCSRepo: &StackVCSRepo{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: project2.ID,
},
})

require.NoError(t, err)
require.NotNil(t, stack2)

t.Run("List without options", func(t *testing.T) {
t.Parallel()

stackList, err := client.Stacks.List(ctx, orgTest.Name, nil)
require.NoError(t, err)

assert.Len(t, stackList.Items, 2)
})

t.Run("List with project filter", func(t *testing.T) {
t.Parallel()

stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
ProjectID: project2.ID,
})
require.NoError(t, err)

assert.Len(t, stackList.Items, 1)
assert.Equal(t, stack2.ID, stackList.Items[0].ID)
})

t.Run("List with name filter", func(t *testing.T) {
t.Parallel()

stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
SearchByName: "zz",
})
require.NoError(t, err)

assert.Len(t, stackList.Items, 1)
assert.Equal(t, stack2.ID, stackList.Items[0].ID)
})

t.Run("List with sort options", func(t *testing.T) {
t.Parallel()

// By name ASC
stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
Sort: StackSortByName,
})
require.NoError(t, err)

assert.Len(t, stackList.Items, 2)
assert.Equal(t, stack1.ID, stackList.Items[0].ID)

// By name DESC
stackList, err = client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
Sort: StackSortByNameDesc,
})
require.NoError(t, err)

assert.Len(t, stackList.Items, 2)
assert.Equal(t, stack2.ID, stackList.Items[0].ID)
})

t.Run("List with pagination", func(t *testing.T) {
t.Parallel()

stackList, err := client.Stacks.List(ctx, orgTest.Name, &StackListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 1,
},
})
require.NoError(t, err)

assert.Len(t, stackList.Items, 1)
assert.Equal(t, 2, stackList.Pagination.TotalPages)
assert.Equal(t, 2, stackList.Pagination.TotalCount)
})
}

func TestStackReadUpdateDelete(t *testing.T) {
skipUnlessBeta(t)

client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
t.Cleanup(orgTestCleanup)

oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
t.Cleanup(cleanup)

stack, err := client.Stacks.Create(ctx, StackCreateOptions{
Name: "test-stack",
VCSRepo: &StackVCSRepo{
Identifier: "brandonc/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
Project: &Project{
ID: orgTest.DefaultProject.ID,
},
})

require.NoError(t, err)
require.NotNil(t, stack)

stackRead, err := client.Stacks.Read(ctx, stack.ID)
require.NoError(t, err)

assert.Equal(t, stack, stackRead)

stackUpdated, err := client.Stacks.Update(ctx, stack.ID, StackUpdateOptions{
Description: String("updated description"),
})

require.NoError(t, err)
require.Equal(t, "updated description", stackUpdated.Description)

err = client.Stacks.Delete(ctx, stack.ID)
require.NoError(t, err)

stackReadAfterDelete, err := client.Stacks.Read(ctx, stack.ID)
require.ErrorIs(t, err, ErrResourceNotFound)
require.Nil(t, stackReadAfterDelete)
}
Loading

0 comments on commit f18ee8e

Please sign in to comment.