Skip to content

Commit

Permalink
Merge pull request #940 from hashicorp/TF-18548
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielMSchmidt authored Jul 31, 2024
2 parents 517bca1 + 4a486e2 commit de2e5b8
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 0 deletions.
26 changes: 26 additions & 0 deletions stack_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package tfe

import (
"bytes"
"context"
"fmt"
"net/url"
Expand All @@ -16,6 +17,9 @@ import (
type StackConfigurations interface {
// ReadConfiguration returns a stack configuration by its ID.
Read(ctx context.Context, id string) (*StackConfiguration, error)

// JSONSchemas returns a byte slice of the JSON schema for the stack configuration.
JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error)
}

type stackConfigurations struct {
Expand All @@ -38,3 +42,25 @@ func (s stackConfigurations) Read(ctx context.Context, id string) (*StackConfigu

return stackConfiguration, nil
}

/**
* Returns the JSON schema for the stack configuration as a byte slice.
* The return value needs to be unmarshalled into a struct to be useful.
* It is meant to be unmarshalled with terraform/internal/command/jsonproivder.Providers.
*/
func (s stackConfigurations) JSONSchemas(ctx context.Context, stackConfigurationID string) ([]byte, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-configurations/%s/json-schemas", url.PathEscape(stackConfigurationID)), nil)
if err != nil {
return nil, err
}

req.Header.Set("Accept", "application/json")

var raw bytes.Buffer
err = req.Do(ctx, &raw)
if err != nil {
return nil, err
}

return raw.Bytes(), nil
}
83 changes: 83 additions & 0 deletions stack_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package tfe

import (
"context"
"encoding/json"
"fmt"
"net/url"
"time"
Expand All @@ -28,6 +29,9 @@ type StackPlans interface {

// Discard discards a stack plan.
Discard(ctx context.Context, stackPlanID string) error

// PlanDescription returns the plan description for a stack plan.
PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error)
}

type StackPlansStatusFilter string
Expand Down Expand Up @@ -64,13 +68,15 @@ type stackPlans struct {

var _ StackPlans = &stackPlans{}

// StackPlanStatusTimestamps are the timestamps of the status changes for a stack
type StackPlanStatusTimestamps struct {
CreatedAt time.Time `jsonapi:"attr,created-at,rfc3339"`
RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"`
PausedAt time.Time `jsonapi:"attr,paused-at,rfc3339"`
FinishedAt time.Time `jsonapi:"attr,finished-at,rfc3339"`
}

// PlanChanges is the summary of the planned changes
type PlanChanges struct {
Add int `jsonapi:"attr,add"`
Total int `jsonapi:"attr,total"`
Expand All @@ -95,6 +101,68 @@ type StackPlan struct {
Stack *Stack `jsonapi:"relation,stack"`
}

// JSONChangeDesc represents a change description of a stack plan / apply operation.
type JSONChangeDesc struct {
FormatVersion uint64 `json:"terraform_stack_change_description"`
Interim bool `json:"interim,omitempty"`
Applyable bool `json:"applyable"`
PlanMode string `json:"plan_mode"`
Components []JSONComponent `json:"components"`
ResourceInstances []JSONResourceInstance `json:"resource_instances"`
DeferredResourceInstances []JSONResourceInstanceDeferral `json:"deferred_resource_instances"`
Outputs map[string]JSONOutput `json:"outputs"`
}

// JSONComponent represents a change description of a single component in a plan.
type JSONComponent struct {
Address string `json:"address"`
ComponentAddress string `json:"component_address"`
InstanceCorrelator string `json:"instance_correlator"`
ComponentCorrelator string `json:"component_correlator"`
Actions []ChangeAction `json:"actions"`
Complete bool `json:"complete"`
}

// ChangeAction are the actions a change can have: no-op, create, read, update, delte, forget.
type ChangeAction string

// JSONResourceInstance is the change description of a single resource instance in a plan.
type JSONResourceInstance struct {
ComponentInstanceCorrelator string `json:"component_instance_correlator"`
ComponentInstanceAddress string `json:"component_instance_address"`
Address string `json:"address"`
PreviousComponentInstanceAddress string `json:"previous_component_instance_address,omitempty"`
PreviousAddress string `json:"previous_address,omitempty"`
DeposedKey string `json:"deposed,omitempty"`
ResourceMode string `json:"mode,omitempty"`
ResourceType string `json:"type"`
ProviderAddr string `json:"provider_name"`
Change Change `json:"change"`
}

// JSONResourceInstanceDeferral is the change description of a single resource instance that is deferred.
type JSONResourceInstanceDeferral struct {
ResourceInstance JSONResourceInstance `json:"resource_instance"`
Deferred JSONDeferred `json:"deferred"`
}

// JSONDeferred contains the reason why a resource instance is deferred: instance_count_unknown, resource_config_unknown, provider_config_unknown, provider_config_unknown, or deferred_prereq.
type JSONDeferred struct {
Reason string `json:"reason"`
}

// JSONOutput is the value of a single output in a plan.
type JSONOutput struct {
Change json.RawMessage `json:"change"`
}

// Change represents the change of a resource instance in a plan.
type Change struct {
Actions []ChangeAction `json:"actions"`
After json.RawMessage `json:"after"`
Before json.RawMessage `json:"before"`
}

func (s stackPlans) Read(ctx context.Context, stackPlanID string) (*StackPlan, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans/%s", url.PathEscape(stackPlanID)), nil)
if err != nil {
Expand Down Expand Up @@ -151,3 +219,18 @@ func (s stackPlans) Cancel(ctx context.Context, stackPlanID string) error {

return req.Do(ctx, nil)
}

func (s stackPlans) PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans/%s/plan-description", url.PathEscape(stackPlanID)), nil)
if err != nil {
return nil, err
}

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

return jd, nil
}
12 changes: 12 additions & 0 deletions stack_plan_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,16 @@ func TestStackPlanList(t *testing.T) {
plan, err := client.StackPlans.Read(ctx, planList.Items[0].ID)
require.NoError(t, err)
require.NotNil(t, plan)

jsonSchema, err := client.StackConfigurations.JSONSchemas(ctx, stackUpdated.LatestStackConfiguration.ID)
require.NoError(t, err)
require.NotNil(t, jsonSchema)

planDesc, err := client.StackPlans.PlanDescription(ctx, planList.Items[0].ID)
require.NoError(t, err)
require.NotNil(t, planDesc)

spo, err := client.StackPlanOperations.Read(ctx, stackUpdated.LatestStackConfiguration.ID)
require.NoError(t, err)
require.NotNil(t, spo)
}
88 changes: 88 additions & 0 deletions stack_plan_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfe

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

// NOTE WELL: This is a beta feature and is subject to change until noted otherwise in the
// release notes.
type StackPlanOperations interface {
// Read returns a stack plan operation by its ID.
Read(ctx context.Context, stackPlanOperationID string) (*StackPlanOperation, error)

// Get Stack Plans from Configuration Version
DownloadEventStream(ctx context.Context, stackPlanOperationID string) ([]byte, error)
}

type stackPlanOperations struct {
client *Client
}

var _ StackPlanOperations = &stackPlanOperations{}

type StackPlanOperation struct {
ID string `jsonapi:"primary,stack-plan-operations"`
Type string `jsonapi:"attr,operation-type"`
Status string `jsonapi:"attr,status"`
EventStreamURL string `jsonapi:"attr,event-stream-url"`
Diagnostics string `jsonapi:"attr,diags"`

// Relations
StackPlan *StackPlan `jsonapi:"relation,stack-plan"`
}

func (s stackPlanOperations) Read(ctx context.Context, stackPlanOperationID string) (*StackPlanOperation, error) {
req, err := s.client.NewRequest("GET", fmt.Sprintf("stack-plans-operations/%s", url.PathEscape(stackPlanOperationID)), nil)
if err != nil {
return nil, err
}

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

return ucs, nil
}

func (s stackPlanOperations) DownloadEventStream(ctx context.Context, eventStreamURL string) ([]byte, error) {
// Create a new request.
req, err := http.NewRequest("GET", eventStreamURL, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)

// Attach the default headers.
for k, v := range s.client.headers {
req.Header[k] = v
}

// Retrieve the next chunk.
resp, err := s.client.http.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// Basic response checking.
if err := checkResponseCode(resp); err != nil {
return nil, err
}

// Read the retrieved chunk.
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return b, nil
}
2 changes: 2 additions & 0 deletions tfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ type Client struct {
StackConfigurations StackConfigurations
StackDeployments StackDeployments
StackPlans StackPlans
StackPlanOperations StackPlanOperations
StateVersionOutputs StateVersionOutputs
StateVersions StateVersions
TaskResults TaskResults
Expand Down Expand Up @@ -470,6 +471,7 @@ func NewClient(cfg *Config) (*Client, error) {
client.StackConfigurations = &stackConfigurations{client: client}
client.StackDeployments = &stackDeployments{client: client}
client.StackPlans = &stackPlans{client: client}
client.StackPlanOperations = &stackPlanOperations{client: client}
client.StateVersionOutputs = &stateVersionOutputs{client: client}
client.StateVersions = &stateVersions{client: client}
client.TaskResults = &taskResults{client: client}
Expand Down

0 comments on commit de2e5b8

Please sign in to comment.