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

More Stacks, StackPlans support #934

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# UNRELEASED

* 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)

# v1.59.0

## Features
Expand Down
68 changes: 67 additions & 1 deletion stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type Stacks interface {

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

// UpdateConfiguration updates the configuration of a stack, triggering stack preparation.
UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error)
}

// stacks implements Stacks.
Expand Down Expand Up @@ -82,7 +85,55 @@ type Stack struct {
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`

// Relationships
Project *Project `jsonapi:"relation,project"`
Project *Project `jsonapi:"relation,project"`
LatestStackConfiguration *StackConfiguration `jsonapi:"relation,latest-stack-configuration"`
}

type StackConfigurationStatusTimestamps struct {
QueuedAt *time.Time `jsonapi:"attr,queued-at,omitempty,rfc3339"`
CompletedAt *time.Time `jsonapi:"attr,completed-at,omitempty,rfc3339"`
PreparingAt *time.Time `jsonapi:"attr,preparing-at,omitempty,rfc3339"`
EnqueueingAt *time.Time `jsonapi:"attr,enqueueing-at,omitempty,rfc3339"`
CanceledAt *time.Time `jsonapi:"attr,canceled-at,omitempty,rfc3339"`
ErroredAt *time.Time `jsonapi:"attr,errored-at,omitempty,rfc3339"`
}
Comment on lines +93 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the more constrained format rfc3339 and not iso8601?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all practical purposes, the API emits dates that fits both formats. For the purposes of parsing dates from HCP Terraform, they should be equivalent. RFC3339 is the slightly more stringent version so I usually choose it


type StackComponent struct {
Name string `json:"name"`
Correlator string `json:"correlator"`
Expanded bool `json:"expanded"`
}

type StackConfiguration struct {
// Attributes
ID string `jsonapi:"primary,stack-configurations"`
Status string `jsonapi:"attr,status"`
StatusTimestamps *StackConfigurationStatusTimestamps `jsonapi:"attr,status-timestamps"`
SequenceNumber int `jsonapi:"attr,sequence-number"`
DeploymentNames []string `jsonapi:"attr,deployment-names"`
ConvergedDeployments []string `jsonapi:"attr,converged-deployments"`
Components []*StackComponent `jsonapi:"attr,components"`
ErrorMessage *string `jsonapi:"attr,error-message"`
EventStreamURL string `jsonapi:"attr,event-stream-url"`
}

type StackDeployment struct {
// Attributes
ID string `jsonapi:"primary,stack-deployments"`
Name string `jsonapi:"attr,name"`
Status string `jsonapi:"attr,status"`
DeployedAt time.Time `jsonapi:"attr,deployed-at,iso8601"`
ErrorsCount int `jsonapi:"attr,errors-count"`
WarningsCount int `jsonapi:"attr,warnings-count"`
PausedCount int `jsonapi:"attr,paused-count"`

// Relationships
CurrentStackState *StackState `jsonapi:"relation,current-stack-state"`
}

type StackState struct {
// Attributes
ID string `jsonapi:"primary,stack-states"`
}

// StackListOptions represents the options for listing stacks.
Expand Down Expand Up @@ -110,6 +161,21 @@ type StackUpdateOptions struct {
VCSRepo *StackVCSRepo `jsonapi:"attr,vcs-repo,omitempty"`
}

func (s stacks) UpdateConfiguration(ctx context.Context, stackID string) (*Stack, error) {
brandonc marked this conversation as resolved.
Show resolved Hide resolved
req, err := s.client.NewRequest("POST", fmt.Sprintf("stacks/%s/actions/update-configuration", 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) List(ctx context.Context, organization string, options *StackListOptions) (*StackList, error) {
if err := options.valid(); err != nil {
return nil, err
Expand Down
37 changes: 37 additions & 0 deletions stack_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tfe

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

// StackConfigurations describes all the stacks configurations-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 StackConfigurations interface {
// ReadConfiguration returns a stack configuration by its ID.
Read(ctx context.Context, id string) (*StackConfiguration, error)
}

type stackConfigurations struct {
client *Client
}

var _ StackConfigurations = &stackConfigurations{}

func (s stackConfigurations) Read(ctx context.Context, id string) (*StackConfiguration, error) {
brandonc marked this conversation as resolved.
Show resolved Hide resolved
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s", url.PathEscape(id)), nil)
if err != nil {
return nil, err
}

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

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

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

// StackDeployments describes all the stacks deployments-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 StackDeployments interface {
// Read returns a stack deployment by its name.
Read(ctx context.Context, stackID, deployment string) (*StackDeployment, error)
}

type stackDeployments struct {
client *Client
}

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

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

return deployment, nil
}
147 changes: 147 additions & 0 deletions stack_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package tfe
import (
"context"
"testing"
"time"

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

stackUpdatedConfig, err := client.Stacks.UpdateConfiguration(ctx, stack.ID)
require.NoError(t, err)
require.Equal(t, stack.Name, stackUpdatedConfig.Name)

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)
}

func pollStackDeployments(t *testing.T, ctx context.Context, client *Client, stackID string) (stack *Stack) {
t.Helper()

// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()

deadline, _ := ctx.Deadline()
t.Logf("Polling stack %q for deployments with deadline of %s", stackID, deadline)

ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack %q had no deployments at deadline", stackID)
case <-ticker.C:
var err error
stack, err = client.Stacks.Read(ctx, stackID)
if err != nil {
t.Fatalf("Failed to read stack %q: %s", stackID, err)
}

t.Logf("Stack %q had %d deployments", stack.ID, len(stack.DeploymentNames))
if len(stack.DeploymentNames) > 0 {
finished = true
}
}
}

return
}

func pollStackDeploymentStatus(t *testing.T, ctx context.Context, client *Client, stackID, deploymentName, status string) {
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()

deadline, _ := ctx.Deadline()
t.Logf("Polling stack %q for deployments with deadline of %s", stackID, deadline)

ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack deployment %s/%s did not have status %q at deadline", stackID, deploymentName, status)
case <-ticker.C:
var err error
deployment, err := client.StackDeployments.Read(ctx, stackID, deploymentName)
if err != nil {
t.Fatalf("Failed to read stack %q: %s", stackID, err)
brandonc marked this conversation as resolved.
Show resolved Hide resolved
}

t.Logf("Stack deployment %s/%s had status %q", stackID, deploymentName, deployment.Status)
if deployment.Status == status {
finished = true
}
}
}
}

func pollStackConfigurationStatus(t *testing.T, ctx context.Context, client *Client, stackConfigID, status string) (stackConfig *StackConfiguration) {
// pollStackDeployments will poll the given stack until it has deployments or the deadline is reached.
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()

deadline, _ := ctx.Deadline()
t.Logf("Polling stack configuration %q for status %q with deadline of %s", stackConfigID, status, deadline)

ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

var err error
for finished := false; !finished; {
t.Log("...")
select {
case <-ctx.Done():
t.Fatalf("Stack configuration %q did not have status %q at deadline", stackConfigID, status)
case <-ticker.C:
stackConfig, err = client.StackConfigurations.Read(ctx, stackConfigID)
if err != nil {
t.Fatalf("Failed to read stack configuration %q: %s", stackConfigID, err)
}

t.Logf("Stack configuration %q had status %q", stackConfigID, stackConfig.Status)
if stackConfig.Status == status {
finished = true
}
}
}

return
}

func TestStackConverged(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

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)

stackUpdated, err := client.Stacks.UpdateConfiguration(ctx, stack.ID)
require.NoError(t, err)
require.NotNil(t, stackUpdated)

deployments := []string{"production", "staging"}

stack = pollStackDeployments(t, ctx, client, stackUpdated.ID)
require.ElementsMatch(t, deployments, stack.DeploymentNames)
require.NotNil(t, stack.LatestStackConfiguration)

for _, deployment := range deployments {
pollStackDeploymentStatus(t, ctx, client, stack.ID, deployment, "paused")
}

pollStackConfigurationStatus(t, ctx, client, stack.LatestStackConfiguration.ID, "converged")
}
Loading
Loading