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

Stacks support (Beta) #920

Merged
merged 2 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading