From ea2a5a1755bce7df2ad5eafc81828d5532f78eda Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 25 Jul 2024 15:38:32 +0200 Subject: [PATCH 1/5] stack: add jsonschema endpoint I decided to not unmarshal this here since moving the type from inside terraform to here seems overkill. --- stack_configuration.go | 26 ++++++++++++++++++++++++++ stack_plan_integration_test.go | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/stack_configuration.go b/stack_configuration.go index 6a6c45e53..f06170c08 100644 --- a/stack_configuration.go +++ b/stack_configuration.go @@ -1,6 +1,7 @@ package tfe import ( + "bytes" "context" "fmt" "net/url" @@ -13,6 +14,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 { @@ -35,3 +39,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 +} diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 0bfe86332..6ff59088d 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -50,4 +50,8 @@ 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) } From 56443018755bdac1601dcfca1b57cd0fa13b3c7f Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 25 Jul 2024 15:53:16 +0200 Subject: [PATCH 2/5] stack: add plan description endpoint --- stack_plan.go | 80 ++++++++++++++++++++++++++++++++++ stack_plan_integration_test.go | 4 ++ 2 files changed, 84 insertions(+) diff --git a/stack_plan.go b/stack_plan.go index 7c235a55a..96ef8077a 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -2,6 +2,7 @@ package tfe import ( "context" + "encoding/json" "fmt" "net/url" "time" @@ -25,6 +26,9 @@ type StackPlans interface { // Discard discards a stack plan. Discard(ctx context.Context, stackPlanID string) error + + // Discard returns the plan description for a stack plan. + PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error) } type StackPlansStatusFilter string @@ -92,6 +96,67 @@ 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"` +} + +type JSONComponent struct { + // FIXME: UI seems to want a "name" that is something more compact + // than the full address, but not sure exactly what that ought to + // be once we consider the possibility of embedded stacks and + // components with for_each set. For now we just return the + // full address pending further discussion. + 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"` +} + +type ChangeAction string + +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"` +} + +type JSONResourceInstanceDeferral struct { + ResourceInstance JSONResourceInstance `json:"resource_instance"` + Deferred JSONDeferred `json:"deferred"` +} + +type JSONDeferred struct { + Reason string `json:"reason"` +} + +type JSONOutput struct { + Change json.RawMessage `json:"change"` +} + +type Change struct { + Actions []ChangeAction `json:"actions"` + After json.RawMessage `json:"after"` + Before json.RawMessage `json:"before"` + // TODO: Add after_sensitive, after_unknown, before_sensitive +} + 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 { @@ -148,3 +213,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 +} diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 6ff59088d..99a1241c4 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -54,4 +54,8 @@ func TestStackPlanList(t *testing.T) { 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) } From efc035626fc6761fcff66b5b8d246557cd51fa1c Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Thu, 25 Jul 2024 16:03:41 +0200 Subject: [PATCH 3/5] stacks: add stack plan operation --- stack_plan_integration_test.go | 4 ++ stack_plan_operation.go | 88 ++++++++++++++++++++++++++++++++++ tfe.go | 2 + 3 files changed, 94 insertions(+) create mode 100644 stack_plan_operation.go diff --git a/stack_plan_integration_test.go b/stack_plan_integration_test.go index 99a1241c4..f773c186b 100644 --- a/stack_plan_integration_test.go +++ b/stack_plan_integration_test.go @@ -58,4 +58,8 @@ func TestStackPlanList(t *testing.T) { 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) } diff --git a/stack_plan_operation.go b/stack_plan_operation.go new file mode 100644 index 000000000..cc77bba66 --- /dev/null +++ b/stack_plan_operation.go @@ -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 +} diff --git a/tfe.go b/tfe.go index b42ef4149..5109156ea 100644 --- a/tfe.go +++ b/tfe.go @@ -165,6 +165,7 @@ type Client struct { StackConfigurations StackConfigurations StackDeployments StackDeployments StackPlans StackPlans + StackPlanOperations StackPlanOperations StateVersionOutputs StateVersionOutputs StateVersions StateVersions TaskResults TaskResults @@ -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} From 06ace7688e15ccfaae4e91a55fafc10c4babc611 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 29 Jul 2024 15:00:31 +0200 Subject: [PATCH 4/5] fix typo Co-authored-by: Luces Huayhuaca <21225410+uturunku1@users.noreply.github.com> --- stack_plan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_plan.go b/stack_plan.go index 96ef8077a..8a528ecc2 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -27,7 +27,7 @@ type StackPlans interface { // Discard discards a stack plan. Discard(ctx context.Context, stackPlanID string) error - // Discard returns the plan description for a stack plan. + // PlanDescription returns the plan description for a stack plan. PlanDescription(ctx context.Context, stackPlanID string) (*JSONChangeDesc, error) } From 4a486e23e97485b40d91ab0e00bdcbf649d240e2 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 29 Jul 2024 16:13:35 +0200 Subject: [PATCH 5/5] add comments --- stack_plan.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stack_plan.go b/stack_plan.go index 8a528ecc2..113191f90 100644 --- a/stack_plan.go +++ b/stack_plan.go @@ -65,6 +65,7 @@ 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"` @@ -72,6 +73,7 @@ type StackPlanStatusTimestamps struct { 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"` @@ -108,12 +110,8 @@ type JSONChangeDesc struct { Outputs map[string]JSONOutput `json:"outputs"` } +// JSONComponent represents a change description of a single component in a plan. type JSONComponent struct { - // FIXME: UI seems to want a "name" that is something more compact - // than the full address, but not sure exactly what that ought to - // be once we consider the possibility of embedded stacks and - // components with for_each set. For now we just return the - // full address pending further discussion. Address string `json:"address"` ComponentAddress string `json:"component_address"` InstanceCorrelator string `json:"instance_correlator"` @@ -122,8 +120,10 @@ type JSONComponent struct { 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"` @@ -137,24 +137,27 @@ type JSONResourceInstance struct { 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"` - // TODO: Add after_sensitive, after_unknown, before_sensitive } func (s stackPlans) Read(ctx context.Context, stackPlanID string) (*StackPlan, error) {