Skip to content

Commit cc4a306

Browse files
authored
Merge pull request #934 from hashicorp/TF-18387-stacks-configuration-deployment-support-in-go-tfe
More Stacks, StackPlans support
2 parents 0e13122 + 30cc1ac commit cc4a306

File tree

8 files changed

+508
-1
lines changed

8 files changed

+508
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# UNRELEASED
22

3+
* Adds more BETA support for `Stacks` resources, which is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc. [#934](https://github.com/hashicorp/go-tfe/pull/934)
4+
35
# v1.59.0
46

57
## Features

stack.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Stacks interface {
2828

2929
// Delete deletes a stack.
3030
Delete(ctx context.Context, stackID string) error
31+
32+
// UpdateConfiguration updates the configuration of a stack, triggering stack preparation.
33+
UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error)
3134
}
3235

3336
// stacks implements Stacks.
@@ -82,7 +85,60 @@ type Stack struct {
8285
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
8386

8487
// Relationships
85-
Project *Project `jsonapi:"relation,project"`
88+
Project *Project `jsonapi:"relation,project"`
89+
LatestStackConfiguration *StackConfiguration `jsonapi:"relation,latest-stack-configuration"`
90+
}
91+
92+
// StackConfigurationStatusTimestamps represents the timestamps for a stack configuration
93+
type StackConfigurationStatusTimestamps struct {
94+
QueuedAt *time.Time `jsonapi:"attr,queued-at,omitempty,rfc3339"`
95+
CompletedAt *time.Time `jsonapi:"attr,completed-at,omitempty,rfc3339"`
96+
PreparingAt *time.Time `jsonapi:"attr,preparing-at,omitempty,rfc3339"`
97+
EnqueueingAt *time.Time `jsonapi:"attr,enqueueing-at,omitempty,rfc3339"`
98+
CanceledAt *time.Time `jsonapi:"attr,canceled-at,omitempty,rfc3339"`
99+
ErroredAt *time.Time `jsonapi:"attr,errored-at,omitempty,rfc3339"`
100+
}
101+
102+
// StackComponent represents a stack component, specified by configuration
103+
type StackComponent struct {
104+
Name string `json:"name"`
105+
Correlator string `json:"correlator"`
106+
Expanded bool `json:"expanded"`
107+
}
108+
109+
// StackConfiguration represents a stack configuration snapshot
110+
type StackConfiguration struct {
111+
// Attributes
112+
ID string `jsonapi:"primary,stack-configurations"`
113+
Status string `jsonapi:"attr,status"`
114+
StatusTimestamps *StackConfigurationStatusTimestamps `jsonapi:"attr,status-timestamps"`
115+
SequenceNumber int `jsonapi:"attr,sequence-number"`
116+
DeploymentNames []string `jsonapi:"attr,deployment-names"`
117+
ConvergedDeployments []string `jsonapi:"attr,converged-deployments"`
118+
Components []*StackComponent `jsonapi:"attr,components"`
119+
ErrorMessage *string `jsonapi:"attr,error-message"`
120+
EventStreamURL string `jsonapi:"attr,event-stream-url"`
121+
}
122+
123+
// StackDeployment represents a stack deployment, specified by configuration
124+
type StackDeployment struct {
125+
// Attributes
126+
ID string `jsonapi:"primary,stack-deployments"`
127+
Name string `jsonapi:"attr,name"`
128+
Status string `jsonapi:"attr,status"`
129+
DeployedAt time.Time `jsonapi:"attr,deployed-at,iso8601"`
130+
ErrorsCount int `jsonapi:"attr,errors-count"`
131+
WarningsCount int `jsonapi:"attr,warnings-count"`
132+
PausedCount int `jsonapi:"attr,paused-count"`
133+
134+
// Relationships
135+
CurrentStackState *StackState `jsonapi:"relation,current-stack-state"`
136+
}
137+
138+
// StackState represents a stack state
139+
type StackState struct {
140+
// Attributes
141+
ID string `jsonapi:"primary,stack-states"`
86142
}
87143

88144
// StackListOptions represents the options for listing stacks.
@@ -110,6 +166,23 @@ type StackUpdateOptions struct {
110166
VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo,omitempty"`
111167
}
112168

169+
// UpdateConfiguration updates the configuration of a stack, triggering stack operations
170+
func (s *stacks) UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error) {
171+
req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/actions/update-configuration", url.PathEscape(stackID)), nil)
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
stack := &Stack{}
177+
err = req.Do(ctx, stack)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
return stack, nil
183+
}
184+
185+
// List returns a list of stacks, optionally filtered by additional paameters.
113186
func (s stacks) List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error) {
114187
if err := options.valid(); err != nil {
115188
return nil, err
@@ -129,6 +202,7 @@ func (s stacks) List(ctx context.Context, organization string, options *StackLis
129202
return sl, nil
130203
}
131204

205+
// Read returns a stack by its ID.
132206
func (s stacks) Read(ctx context.Context, stackID string) (*Stack, error) {
133207
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), nil)
134208
if err != nil {
@@ -144,6 +218,7 @@ func (s stacks) Read(ctx context.Context, stackID string) (*Stack, error) {
144218
return stack, nil
145219
}
146220

221+
// Create creates a new stack.
147222
func (s stacks) Create(ctx context.Context, options StackCreateOptions) (*Stack, error) {
148223
if err := options.valid(); err != nil {
149224
return nil, err
@@ -163,6 +238,7 @@ func (s stacks) Create(ctx context.Context, options StackCreateOptions) (*Stack,
163238
return stack, nil
164239
}
165240

241+
// Update updates a stack.
166242
func (s stacks) Update(ctx context.Context, stackID string, options StackUpdateOptions) (*Stack, error) {
167243
req, err := s.client.NewRequest("PATCH", fmt.Sprintf("stacks/%s", url.PathEscape(stackID)), &options)
168244
if err != nil {
@@ -178,6 +254,7 @@ func (s stacks) Update(ctx context.Context, stackID string, options StackUpdateO
178254
return stack, nil
179255
}
180256

257+
// Delete deletes a stack.
181258
func (s stacks) Delete(ctx context.Context, stackID string) error {
182259
req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/delete", url.PathEscape(stackID)), nil)
183260
if err != nil {

stack_configuration.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package tfe
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
)
8+
9+
// StackConfigurations describes all the stacks configurations-related methods that the
10+
// HCP Terraform API supports.
11+
// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the
12+
// release notes.
13+
type StackConfigurations interface {
14+
// ReadConfiguration returns a stack configuration by its ID.
15+
Read(ctx context.Context, id string) (*StackConfiguration, error)
16+
}
17+
18+
type stackConfigurations struct {
19+
client *Client
20+
}
21+
22+
var _ StackConfigurations = &stackConfigurations{}
23+
24+
func (s stackConfigurations) Read(ctx context.Context, id string) (*StackConfiguration, error) {
25+
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s", url.PathEscape(id)), nil)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
stackConfiguration := &StackConfiguration{}
31+
err = req.Do(ctx, stackConfiguration)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
return stackConfiguration, nil
37+
}

stack_deployments.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package tfe
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
)
8+
9+
// StackDeployments describes all the stacks deployments-related methods that the
10+
// HCP Terraform API supports.
11+
// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the
12+
// release notes.
13+
type StackDeployments interface {
14+
// Read returns a stack deployment by its name.
15+
Read(ctx context.Context, stackID, deployment string) (*StackDeployment, error)
16+
}
17+
18+
type stackDeployments struct {
19+
client *Client
20+
}
21+
22+
func (s stackDeployments) Read(ctx context.Context, stackID, deploymentName string) (*StackDeployment, error) {
23+
req, err := s.client.NewRequest("GET", fmt.Sprintf("stacks/%s/stack-deployments/%s", url.PathEscape(stackID), url.PathEscape(deploymentName)), nil)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
deployment := &StackDeployment{}
29+
err = req.Do(ctx, deployment)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
return deployment, nil
35+
}

stack_integration_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package tfe
66
import (
77
"context"
88
"testing"
9+
"time"
910

1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
@@ -166,10 +167,156 @@ func TestStackReadUpdateDelete(t *testing.T) {
166167
require.NoError(t, err)
167168
require.Equal(t, "updated description", stackUpdated.Description)
168169

170+
stackUpdatedConfig, err := client.Stacks.UpdateConfiguration(ctx, stack.ID)
171+
require.NoError(t, err)
172+
require.Equal(t, stack.Name, stackUpdatedConfig.Name)
173+
169174
err = client.Stacks.Delete(ctx, stack.ID)
170175
require.NoError(t, err)
171176

172177
stackReadAfterDelete, err := client.Stacks.Read(ctx, stack.ID)
173178
require.ErrorIs(t, err, ErrResourceNotFound)
174179
require.Nil(t, stackReadAfterDelete)
175180
}
181+
182+
func pollStackDeployments(t *testing.T, ctx context.Context, client *Client, stackID string) (stack *Stack) {
183+
t.Helper()
184+
185+
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
186+
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
187+
defer cancel()
188+
189+
deadline, _ := ctx.Deadline()
190+
t.Logf("Polling stack %q for deployments with deadline of %s", stackID, deadline)
191+
192+
ticker := time.NewTicker(2 * time.Second)
193+
defer ticker.Stop()
194+
195+
for finished := false; !finished; {
196+
t.Log("...")
197+
select {
198+
case <-ctx.Done():
199+
t.Fatalf("Stack %q had no deployments at deadline", stackID)
200+
case <-ticker.C:
201+
var err error
202+
stack, err = client.Stacks.Read(ctx, stackID)
203+
if err != nil {
204+
t.Fatalf("Failed to read stack %q: %s", stackID, err)
205+
}
206+
207+
t.Logf("Stack %q had %d deployments", stack.ID, len(stack.DeploymentNames))
208+
if len(stack.DeploymentNames) > 0 {
209+
finished = true
210+
}
211+
}
212+
}
213+
214+
return
215+
}
216+
217+
func pollStackDeploymentStatus(t *testing.T, ctx context.Context, client *Client, stackID, deploymentName, status string) {
218+
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
219+
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
220+
defer cancel()
221+
222+
deadline, _ := ctx.Deadline()
223+
t.Logf("Polling stack %q for deployments with deadline of %s", stackID, deadline)
224+
225+
ticker := time.NewTicker(2 * time.Second)
226+
defer ticker.Stop()
227+
228+
for finished := false; !finished; {
229+
t.Log("...")
230+
select {
231+
case <-ctx.Done():
232+
t.Fatalf("Stack deployment %s/%s did not have status %q at deadline", stackID, deploymentName, status)
233+
case <-ticker.C:
234+
var err error
235+
deployment, err := client.StackDeployments.Read(ctx, stackID, deploymentName)
236+
if err != nil {
237+
t.Fatalf("Failed to read stack deployment %s/%s: %s", stackID, deploymentName, err)
238+
}
239+
240+
t.Logf("Stack deployment %s/%s had status %q", stackID, deploymentName, deployment.Status)
241+
if deployment.Status == status {
242+
finished = true
243+
}
244+
}
245+
}
246+
}
247+
248+
func pollStackConfigurationStatus(t *testing.T, ctx context.Context, client *Client, stackConfigID, status string) (stackConfig *StackConfiguration) {
249+
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
250+
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
251+
defer cancel()
252+
253+
deadline, _ := ctx.Deadline()
254+
t.Logf("Polling stack configuration %q for status %q with deadline of %s", stackConfigID, status, deadline)
255+
256+
ticker := time.NewTicker(2 * time.Second)
257+
defer ticker.Stop()
258+
259+
var err error
260+
for finished := false; !finished; {
261+
t.Log("...")
262+
select {
263+
case <-ctx.Done():
264+
t.Fatalf("Stack configuration %q did not have status %q at deadline", stackConfigID, status)
265+
case <-ticker.C:
266+
stackConfig, err = client.StackConfigurations.Read(ctx, stackConfigID)
267+
if err != nil {
268+
t.Fatalf("Failed to read stack configuration %q: %s", stackConfigID, err)
269+
}
270+
271+
t.Logf("Stack configuration %q had status %q", stackConfigID, stackConfig.Status)
272+
if stackConfig.Status == status {
273+
finished = true
274+
}
275+
}
276+
}
277+
278+
return
279+
}
280+
281+
func TestStackConverged(t *testing.T) {
282+
skipUnlessBeta(t)
283+
284+
client := testClient(t)
285+
ctx := context.Background()
286+
287+
orgTest, orgTestCleanup := createOrganization(t, client)
288+
t.Cleanup(orgTestCleanup)
289+
290+
oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil)
291+
t.Cleanup(cleanup)
292+
293+
stack, err := client.Stacks.Create(ctx, StackCreateOptions{
294+
Name: "test-stack",
295+
VCSRepo: &StackVCSRepo{
296+
Identifier: "brandonc/pet-nulls-stack",
297+
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
298+
},
299+
Project: &Project{
300+
ID: orgTest.DefaultProject.ID,
301+
},
302+
})
303+
304+
require.NoError(t, err)
305+
require.NotNil(t, stack)
306+
307+
stackUpdated, err := client.Stacks.UpdateConfiguration(ctx, stack.ID)
308+
require.NoError(t, err)
309+
require.NotNil(t, stackUpdated)
310+
311+
deployments := []string{"production", "staging"}
312+
313+
stack = pollStackDeployments(t, ctx, client, stackUpdated.ID)
314+
require.ElementsMatch(t, deployments, stack.DeploymentNames)
315+
require.NotNil(t, stack.LatestStackConfiguration)
316+
317+
for _, deployment := range deployments {
318+
pollStackDeploymentStatus(t, ctx, client, stack.ID, deployment, "paused")
319+
}
320+
321+
pollStackConfigurationStatus(t, ctx, client, stack.LatestStackConfiguration.ID, "converged")
322+
}

0 commit comments

Comments
 (0)