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

StackSources #957

Merged
merged 3 commits into from
Aug 14, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Unreleased

## Enhancements

* Adds more BETA support for `Stacks` resources, which is is EXPERIMENTAL, SUBJECT TO CHANGE, and may not be available to all users by @brandonc

# v1.62.0

## Bug Fixes
Expand Down
1 change: 1 addition & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ func createOrganization(t *testing.T, client *Client) (*Organization, func()) {
Name: String("tst-" + randomString(t)),
Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))),
CostEstimationEnabled: Bool(true),
StacksEnabled: Bool(true),
})
}

Expand Down
4 changes: 4 additions & 0 deletions organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ type OrganizationCreateOptions struct {

// Optional: DefaultExecutionMode the default execution mode for workspaces
DefaultExecutionMode *string `jsonapi:"attr,default-execution-mode,omitempty"`

// Optional: StacksEnabled toggles whether stacks are enabled for the organization. This setting
// is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
StacksEnabled *bool `jsonapi:"attr,stacks-enabled,omitempty"`
}

// OrganizationUpdateOptions represents the options for updating an organization.
Expand Down
95 changes: 95 additions & 0 deletions stack_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfe

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

// StackSources describes all the stack-sources 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 StackSources interface {
// Read retrieves a stack source by its ID.
Read(ctx context.Context, stackSourceID string) (*StackSource, error)

// CreateAndUpload packages and uploads the specified Terraform Stacks
// configuration files in association with a Stack.
CreateAndUpload(ctx context.Context, stackID string, path string) (*StackSource, error)

// UploadTarGzip is used to upload Terraform configuration files contained a tar gzip archive.
// Any stream implementing io.Reader can be passed into this method. This method is also
// particularly useful for tar streams created by non-default go-slug configurations.
//
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error
}

var _ StackSources = (*stackSources)(nil)

type stackSources struct {
client *Client
}

// StackSource represents a source of Terraform Stacks configuration files.
type StackSource struct {
ID string `jsonapi:"primary,stack-sources"`
UploadURL *string `jsonapi:"attr,upload-url"`
StackConfiguration *StackConfiguration `jsonapi:"relation,stack-configuration"`
Stack *Stack `jsonapi:"relation,stack"`
}

// Read retrieves a stack source by its ID.
func (s *stackSources) Read(ctx context.Context, stackSourceID string) (*StackSource, error) {
u := fmt.Sprintf("stack-sources/%s", url.PathEscape(stackSourceID))
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}

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

return ss, nil
}

// CreateAndUpload packages and uploads the specified Terraform Stacks
// configuration files in association with a Stack.
func (s *stackSources) CreateAndUpload(ctx context.Context, stackID, path string) (*StackSource, error) {
u := fmt.Sprintf("stacks/%s/stack-sources", url.PathEscape(stackID))
req, err := s.client.NewRequest("POST", u, nil)
if err != nil {
return nil, err
}

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

body, err := packContents(path)
if err != nil {
return nil, err
}

return ss, s.UploadTarGzip(ctx, *ss.UploadURL, body)
}

// UploadTarGzip is used to upload Terraform configuration files contained a tar gzip archive.
// Any stream implementing io.Reader can be passed into this method. This method is also
// particularly useful for tar streams created by non-default go-slug configurations.
//
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
func (s *stackSources) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
return s.client.doForeignPUTRequest(ctx, uploadURL, archive)
}
66 changes: 66 additions & 0 deletions stack_source_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfe

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestStackSourceCreateUploadAndRead(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{
Project: orgTest.DefaultProject,
Name: "test-stack",
VCSRepo: &StackVCSRepo{
Identifier: "hashicorp-guides/pet-nulls-stack",
OAuthTokenID: oauthClient.OAuthTokens[0].ID,
},
})
require.NoError(t, err)

ss, err := client.StackSources.CreateAndUpload(ctx, stack.ID, "test-fixtures/stack-source")
require.NoError(t, err)
require.NotNil(t, ss)
require.Nil(t, ss.StackConfiguration)

ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()

done := make(chan struct{})
go func() {
for {
ss, err = client.StackSources.Read(ctx, ss.ID)
require.NoError(t, err)

if ss.StackConfiguration != nil {
done <- struct{}{}
return
}

time.Sleep(2 * time.Second)
}
}()

select {
case <-done:
t.Logf("Found stack source configuration %q", ss.StackConfiguration.ID)
return
case <-ctx.Done():
require.Fail(t, "timed out waiting for stack source to be processed")
}
}
1 change: 1 addition & 0 deletions test-fixtures/stack-source/.terraform-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.10.0-alpha20240807
50 changes: 50 additions & 0 deletions test-fixtures/stack-source/components.tfstack.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

variable "prefix" {
type = string
}

variable "instances" {
type = number
}

required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.5.1"
}

null = {
source = "hashicorp/null"
version = "~> 3.2.2"
}
}

provider "random" "this" {}
provider "null" "this" {}

component "pet" {
source = "./pet"

inputs = {
prefix = var.prefix
}

providers = {
random = provider.random.this
}
}

component "nulls" {
source = "./nulls"

inputs = {
pet = component.pet.name
instances = var.instances
}

providers = {
null = provider.null.this
}
}
16 changes: 16 additions & 0 deletions test-fixtures/stack-source/deployments.tfdeploy.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

deployment "simple" {
inputs = {
prefix = "simple"
instances = 1
}
}

deployment "complex" {
inputs = {
prefix = "complex"
instances = 3
}
}
31 changes: 31 additions & 0 deletions test-fixtures/stack-source/nulls/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "3.1.1"
}
}
}

variable "pet" {
type = string
}

variable "instances" {
type = number
}

resource "null_resource" "this" {
count = var.instances

triggers = {
pet = var.pet
}
}

output "ids" {
value = [for n in null_resource.this: n.id]
}
24 changes: 24 additions & 0 deletions test-fixtures/stack-source/pet/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "3.3.2"
}
}
}

variable "prefix" {
type = string
}

resource "random_pet" "this" {
prefix = var.prefix
length = 3
}

output "name" {
value = random_pet.this.id
}
2 changes: 2 additions & 0 deletions tfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ type Client struct {
StackDeployments StackDeployments
StackPlans StackPlans
StackPlanOperations StackPlanOperations
StackSources StackSources
StateVersionOutputs StateVersionOutputs
StateVersions StateVersions
TaskResults TaskResults
Expand Down Expand Up @@ -472,6 +473,7 @@ func NewClient(cfg *Config) (*Client, error) {
client.StackDeployments = &stackDeployments{client: client}
client.StackPlans = &stackPlans{client: client}
client.StackPlanOperations = &stackPlanOperations{client: client}
client.StackSources = &stackSources{client: client}
client.StateVersionOutputs = &stateVersionOutputs{client: client}
client.StateVersions = &stateVersions{client: client}
client.TaskResults = &taskResults{client: client}
Expand Down
Loading