Skip to content

Commit db956c4

Browse files
authored
Add new Stacks resources (#1226)
* feat: add StackStates resource * feat: add StackDeployments resource * feat: add StackDiagnostics resources * chore: remove beta notice from stacks resources * chore: update changelog * tests: add stack state tests * tests: add stack diagnostic tests * tests: remove beta gate for stacks integration tests * chore: update changelog * chore: incorporate review feedback * chore: incorporate review feedback * chore: incorporate review feedback * add Links field to StackState struct * tests: all stacks tests passing * put skipUnlessBeta calls back in place
1 parent ced5cbb commit db956c4

18 files changed

+922
-112
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
* Add `Sort` option to `Agents` and `AgentPools`, by @twitnithegirl [#1229](https://github.com/hashicorp/go-tfe/pull/1229)
66
* Add serialization for `StacksEnabled` field, `CanEnableStacks` & `CanCreateProject` permissions on Organization Read by @a-anurag27 [#1230](https://github.com/hashicorp/go-tfe/pull/1230)
7+
* Adds new stacks resources `StackDeployments`, `StackDiagnostics`, and `StackStates`, by @ctrombley [#1226](https://github.com/hashicorp/go-tfe/pull/1226)
8+
* Adds new `Diagnostics` methods to `StackConfigurations`, and `StackDeploymentSteps`, by @ctrombley [#1226](https://github.com/hashicorp/go-tfe/pull/1226)
9+
* Adds new `Artifacts` method to `StackDeploymentSteps`, by @ctrombley [#1226](https://github.com/hashicorp/go-tfe/pull/1226)
10+
* Add serialization for `StacksEnabled` field, `CanEnableStacks` & `CanCreateProject` permissions on Organization Read by @a-anurag27 [#1230](https://github.com/hashicorp/go-tfe/pull/1230)
711

812
# v1.94.0
913

errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ var (
251251
ErrInvalidHYOKCustomerKeyVersion = errors.New("invalid value for HYOK Customer key version ID")
252252

253253
ErrInvalidHYOKEncryptedDataKey = errors.New("invalid value for HYOK encrypted data key ID")
254+
255+
ErrInvalidStackID = errors.New("invalid value for stack ID")
254256
)
255257

256258
var (

request.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,58 @@ func (r *ClientRequest) DoJSON(ctx context.Context, model any) error {
144144

145145
return json.NewDecoder(resp.Body).Decode(model)
146146
}
147+
148+
// DoRaw exposes the underlying io.ReadCloser for the response body.
149+
// The caller is responsible for closing the ReadCloser and unmarshaling the
150+
// results.
151+
func (r *ClientRequest) DoRaw(ctx context.Context) (io.ReadCloser, error) {
152+
// Wait will block until the limiter can obtain a new token
153+
// or returns an error if the given context is canceled.
154+
if r.limiter != nil {
155+
if err := r.limiter.Wait(ctx); err != nil {
156+
return nil, err
157+
}
158+
}
159+
160+
// Add the context to the request.
161+
contextReq := r.retryableRequest.WithContext(ctx)
162+
163+
// If the caller provided a response header hook then we'll call it
164+
// once we have a response.
165+
respHeaderHook, err := contextResponseHeaderHook(ctx)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
// Execute the request and check the response.
171+
resp, err := r.http.Do(contextReq)
172+
if resp != nil {
173+
// We call the callback whenever there's any sort of response,
174+
// even if it's returned in conjunction with an error.
175+
respHeaderHook(resp.StatusCode, resp.Header)
176+
}
177+
if err != nil {
178+
// If we got an error, and the context has been canceled,
179+
// the context's error is probably more useful.
180+
select {
181+
case <-ctx.Done():
182+
return nil, ctx.Err()
183+
default:
184+
return nil, err
185+
}
186+
}
187+
188+
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
189+
// Close the body here since we won't be returning it to the caller.
190+
resp.Body.Close()
191+
return nil, fmt.Errorf("error HTTP response: %d", resp.StatusCode)
192+
} else if resp.StatusCode == 304 {
193+
// Got a "Not Modified" response, but we can't return a model because there is no response body.
194+
// This is necessary to support the IPRanges endpoint, which has the peculiar behavior
195+
// of not returning content but allowing a 304 response by optionally sending an
196+
// If-Modified-Since header.
197+
return nil, nil
198+
}
199+
200+
return resp.Body, nil
201+
}

stack.go

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
)
1212

1313
// Stacks describes all the stacks-related methods that the HCP Terraform API supports.
14-
// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the
15-
// release notes.
1614
type Stacks interface {
1715
// List returns a list of stacks, optionally filtered by project.
1816
List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error)
@@ -138,22 +136,15 @@ type StackConfiguration struct {
138136
IngressAttributes *IngressAttributes `jsonapi:"relation,ingress-attributes"`
139137
}
140138

141-
// StackState represents a stack state
142-
type StackState struct {
143-
// Attributes
144-
ID string `jsonapi:"primary,stack-states"`
145-
Description string `jsonapi:"attr,description"`
146-
Generation int `jsonapi:"attr,generation"`
147-
Status string `jsonapi:"attr,status"`
148-
Deployment string `jsonapi:"attr,deployment"`
149-
Components string `jsonapi:"attr,components"`
150-
IsCurrent bool `jsonapi:"attr,is-current"`
151-
ResourceInstanceCount int `jsonapi:"attr,resource-instance-count"`
139+
// StackIncludeOpt represents the include options for a stack.
140+
type StackIncludeOpt string
152141

153-
// Relationships
154-
Stack *Stack `jsonapi:"relation,stack"`
155-
StackDeploymentRun *StackDeploymentRun `jsonapi:"relation,stack-deployment-run"`
156-
}
142+
const (
143+
StackIncludeOrganization StackIncludeOpt = "organization"
144+
StackIncludeProject StackIncludeOpt = "project"
145+
StackIncludeLatestStackConfiguration StackIncludeOpt = "latest_stack_configuration"
146+
StackIncludeStackDiagnostics StackIncludeOpt = "latest_stack_configuration.stack_diagnostics"
147+
)
157148

158149
// StackListOptions represents the options for listing stacks.
159150
type StackListOptions struct {

stack_configuration.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import (
1414

1515
// StackConfigurations describes all the stacks configurations-related methods that the
1616
// HCP Terraform API supports.
17-
// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the
18-
// release notes.
1917
type StackConfigurations interface {
2018
// CreateAndUpload packages and uploads the specified Terraform Stacks
2119
// configuration files in association with a Stack.
@@ -28,7 +26,7 @@ type StackConfigurations interface {
2826
Read(ctx context.Context, id string) (*StackConfiguration, error)
2927

3028
// ListStackConfigurations returns a list of stack configurations for a stack.
31-
List(ctx context.Context, stackID string, options *StackConfigurationListOptions) (*StackConfigurationList, error)
29+
List(ctx context.Context, stackID string, opts *StackConfigurationListOptions) (*StackConfigurationList, error)
3230

3331
// JSONSchemas returns a byte slice of the JSON schema for the stack configuration.
3432
JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error)
@@ -42,6 +40,9 @@ type StackConfigurations interface {
4240
// stack configuration as it progresses, until that status is "<status>",
4341
// "errored", "canceled".
4442
AwaitStatus(ctx context.Context, stackConfigurationID string, status StackConfigurationStatus) <-chan WaitForStatusResult
43+
44+
// Diagnostics returns the diagnostics for this stack configuration.
45+
Diagnostics(ctx context.Context, stackConfigurationID string) (*StackDiagnosticsList, error)
4546
}
4647

4748
type StackConfigurationStatus string
@@ -254,3 +255,19 @@ func (s stackConfigurations) pollForUploadURL(ctx context.Context, stackConfigur
254255
func (s stackConfigurations) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
255256
return s.client.doForeignPUTRequest(ctx, uploadURL, archive)
256257
}
258+
259+
// Diagnostics returns the diagnostics for this stack configuration.
260+
func (s stackConfigurations) Diagnostics(ctx context.Context, stackConfigurationID string) (*StackDiagnosticsList, error) {
261+
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/stack-diagnostics", url.PathEscape(stackConfigurationID)), nil)
262+
263+
if err != nil {
264+
return nil, err
265+
}
266+
267+
diagnostics := &StackDiagnosticsList{}
268+
err = req.Do(ctx, diagnostics)
269+
if err != nil {
270+
return nil, err
271+
}
272+
return diagnostics, nil
273+
}

stack_configuration_integration_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,66 @@ func TestStackConfigurationCreateUploadAndRead(t *testing.T) {
122122
require.Fail(t, "timed out waiting for stack configuration to be processed")
123123
}
124124
}
125+
126+
func TestStackConfigurationDiagnostics(t *testing.T) {
127+
skipUnlessBeta(t)
128+
129+
client := testClient(t)
130+
ctx := context.Background()
131+
132+
orgTest, orgTestCleanup := createOrganization(t, client)
133+
t.Cleanup(orgTestCleanup)
134+
135+
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
136+
t.Cleanup(cleanup)
137+
138+
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
139+
140+
Project: orgTest.DefaultProject,
141+
Name: "test-stack",
142+
143+
VCSRepo: &StackVCSRepoOptions{
144+
Identifier: "ctrombley/linked-stacks-demo-network",
145+
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
146+
Branch: "diagnostics", // This branch will produce diagnostics
147+
},
148+
})
149+
require.NoError(t, err)
150+
require.NotNil(t, stack)
151+
152+
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
153+
require.NoError(t, err)
154+
require.NotNil(t, stackUpdated)
155+
156+
stackUpdated, err = client.Stacks.Read(ctx, stackUpdated.ID)
157+
require.NoError(t, err)
158+
require.NotNil(t, stackUpdated.LatestStackConfiguration)
159+
160+
pollStackConfigurationStatus(t, ctx, client, stackUpdated.LatestStackConfiguration.ID, "failed")
161+
162+
t.Run("Diagnostics with valid ID", func(t *testing.T) {
163+
diags, err := client.StackConfigurations.Diagnostics(ctx, stackUpdated.LatestStackConfiguration.ID)
164+
assert.NoError(t, err)
165+
require.NotEmpty(t, diags.Items)
166+
167+
diag := diags.Items[0]
168+
169+
assert.NotEmpty(t, diag.ID)
170+
assert.NotEmpty(t, diag.Severity)
171+
assert.NotEmpty(t, diag.Summary)
172+
assert.NotEmpty(t, diag.Detail)
173+
assert.NotEmpty(t, diag.Diags)
174+
assert.False(t, diag.Acknowledged)
175+
assert.Nil(t, diag.AcknowledgedAt)
176+
assert.NotZero(t, diag.CreatedAt)
177+
178+
assert.Nil(t, diag.StackDeploymentStep)
179+
assert.NotNil(t, diag.StackConfiguration)
180+
assert.Nil(t, diag.AcknowledgedBy)
181+
})
182+
183+
t.Run("Diagnostics with invalid ID", func(t *testing.T) {
184+
_, err := client.StackConfigurations.Diagnostics(ctx, "invalid-id")
185+
require.Error(t, err)
186+
})
187+
}

stack_deployment.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package tfe
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
)
8+
9+
type StackDeployments interface {
10+
// List returns a list of stack deployments for a given stack.
11+
List(ctx context.Context, stackID string, opts *StackDeploymentListOptions) (*StackDeploymentList, error)
12+
}
13+
14+
type StackDeployment struct {
15+
// Attributes
16+
ID string `jsonapi:"primary,stack-deployments"`
17+
Name string `jsonapi:"attr,name"`
18+
19+
// Relationships
20+
Stack *Stack `jsonapi:"relation,stack"`
21+
LatestDeploymentRun *StackDeploymentRun `jsonapi:"relation,latest-deployment-run"`
22+
}
23+
24+
type stackDeployments struct {
25+
client *Client
26+
}
27+
28+
type StackDeploymentListOptions struct {
29+
ListOptions
30+
}
31+
32+
type StackDeploymentList struct {
33+
*Pagination
34+
Items []*StackDeployment
35+
}
36+
37+
func (s stackDeployments) List(ctx context.Context, stackID string, opts *StackDeploymentListOptions) (*StackDeploymentList, error) {
38+
if !validStringID(&stackID) {
39+
return nil, ErrInvalidStackID
40+
}
41+
42+
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-deployments", url.PathEscape(stackID)), opts)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
var deployments StackDeploymentList
48+
if err := req.Do(ctx, &deployments); err != nil {
49+
return nil, err
50+
}
51+
52+
return &deployments, nil
53+
}

stack_deployment_groups_integration_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
func TestStackDeploymentGroupsList(t *testing.T) {
1515
skipUnlessBeta(t)
16+
1617
client := testClient(t)
1718
ctx := context.Background()
1819

@@ -78,6 +79,7 @@ func TestStackDeploymentGroupsList(t *testing.T) {
7879

7980
func TestStackDeploymentGroupsRead(t *testing.T) {
8081
skipUnlessBeta(t)
82+
8183
client := testClient(t)
8284
ctx := context.Background()
8385

@@ -222,7 +224,7 @@ func TestStackDeploymentGroupsRerun(t *testing.T) {
222224
err = client.StackDeploymentGroups.ApproveAllPlans(ctx, deploymentGroupID)
223225
require.NoError(t, err)
224226

225-
pollStackDeploymentRunForDeployingStatus(t, ctx, client, deploymentRuns.Items[0].ID)
227+
pollStackDeploymentRunStatus(t, ctx, client, deploymentRuns.Items[0].ID, "deploying")
226228

227229
deploymentRunIds := []string{deploymentRuns.Items[0].ID}
228230
for _, dr := range deploymentRuns.Items {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package tfe
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestStackDeploymentsList(t *testing.T) {
11+
skipUnlessBeta(t)
12+
13+
client := testClient(t)
14+
ctx := context.Background()
15+
16+
orgTest, orgTestCleanup := createOrganization(t, client)
17+
t.Cleanup(orgTestCleanup)
18+
19+
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
20+
t.Cleanup(cleanup)
21+
22+
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
23+
Name: "aa-test-stack",
24+
VCSRepo: &StackVCSRepoOptions{
25+
Identifier: "hashicorp-guides/pet-nulls-stack",
26+
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
27+
},
28+
Project: &Project{
29+
ID: orgTest.DefaultProject.ID,
30+
},
31+
})
32+
require.NoError(t, err)
33+
require.NotNil(t, stack)
34+
35+
stackUpdated, err := client.Stacks.FetchLatestFromVcs(ctx, stack.ID)
36+
require.NoError(t, err)
37+
require.NotNil(t, stackUpdated)
38+
39+
stackUpdated = pollStackDeploymentGroups(t, ctx, client, stackUpdated.ID)
40+
41+
t.Run("List with valid options", func(t *testing.T) {
42+
opts := &StackDeploymentListOptions{
43+
ListOptions: ListOptions{
44+
PageNumber: 1,
45+
PageSize: 1,
46+
},
47+
}
48+
sdl, err := client.StackDeployments.List(ctx, stackUpdated.ID, opts)
49+
require.NoError(t, err)
50+
require.NotNil(t, sdl)
51+
require.Len(t, sdl.Items, 1)
52+
})
53+
54+
t.Run("List with invalid options", func(t *testing.T) {
55+
opts := &StackDeploymentListOptions{
56+
ListOptions: ListOptions{
57+
PageNumber: -1,
58+
PageSize: -1,
59+
},
60+
}
61+
62+
_, err := client.StackDeployments.List(ctx, stackUpdated.ID, opts)
63+
require.Error(t, err)
64+
})
65+
}

0 commit comments

Comments
 (0)