-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #920 from hashicorp/TF-17012-stacks-support-in-go-tfe
Stacks support (Beta)
- Loading branch information
Showing
4 changed files
with
387 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.