From 07f6621091eac3b9dad5dcfeac3ec0f5b7e92e8e Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Thu, 15 Feb 2024 10:45:47 +0100 Subject: [PATCH] stacks: include resources in state when calculating required providers (#34645) * stacks: include resources in state when calculating required providers * also support apply time * add copywrite headers --- internal/stacks/stackplan/component.go | 19 ++ internal/stacks/stackplan/plan.go | 16 ++ internal/stacks/stackruntime/apply_test.go | 244 ++++++++++++++++++ .../internal/stackeval/component_instance.go | 34 ++- .../stackruntime/internal/stackeval/main.go | 21 ++ internal/stacks/stackruntime/plan_test.go | 116 +++++++++ .../test/empty-component/empty-component.tf | 7 + .../missing-providers.tfstack.hcl | 13 + .../valid-providers.tfstack.hcl | 15 ++ .../with-single-resource.tf | 19 ++ .../with-single-resource.tfstack.hcl | 22 ++ internal/stacks/stackstate/state.go | 27 +- internal/stacks/stackstate/state_builder.go | 68 +++++ 13 files changed, 614 insertions(+), 7 deletions(-) create mode 100644 internal/stacks/stackruntime/apply_test.go create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf create mode 100644 internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl create mode 100644 internal/stacks/stackstate/state_builder.go diff --git a/internal/stacks/stackplan/component.go b/internal/stacks/stackplan/component.go index eb02df76db..81d737c52d 100644 --- a/internal/stacks/stackplan/component.go +++ b/internal/stacks/stackplan/component.go @@ -118,3 +118,22 @@ func (c *Component) ForModulesRuntime() (*plans.Plan, error) { return plan, nil } + +// RequiredProviderInstances returns a description of all the provider instance +// slots that are required to satisfy the resource instances planned for this +// component. +// +// See also stackstate.State.RequiredProviderInstances and +// stackeval.ComponentConfig.RequiredProviderInstances for similar functions +// that retrieve the provider instances for a components in the config and in +// the state. +func (c *Component) RequiredProviderInstances() addrs.Set[addrs.RootProviderConfig] { + providerInstances := addrs.MakeSet[addrs.RootProviderConfig]() + for _, elem := range c.ResourceInstanceProviderConfig.Elems { + providerInstances.Add(addrs.RootProviderConfig{ + Provider: elem.Value.Provider, + Alias: elem.Value.Alias, + }) + } + return providerInstances +} diff --git a/internal/stacks/stackplan/plan.go b/internal/stacks/stackplan/plan.go index 96c383e2f5..7ddfd66c39 100644 --- a/internal/stacks/stackplan/plan.go +++ b/internal/stacks/stackplan/plan.go @@ -7,6 +7,7 @@ import ( "github.com/zclconf/go-cty/cty" "google.golang.org/protobuf/types/known/anypb" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" ) @@ -45,3 +46,18 @@ type Plan struct { // nested component instances from embedded stacks. Components collections.Map[stackaddrs.AbsComponentInstance, *Component] } + +// RequiredProviderInstances returns a description of all of the provider +// instance slots that are required to satisfy the resource instances +// belonging to the given component instance. +// +// See also stackeval.ComponentConfig.RequiredProviderInstances for a similar +// function that operates on the configuration of a component instance rather +// than the plan of one. +func (p *Plan) RequiredProviderInstances(addr stackaddrs.AbsComponentInstance) addrs.Set[addrs.RootProviderConfig] { + component, ok := p.Components.GetOk(addr) + if !ok { + return addrs.MakeSet[addrs.RootProviderConfig]() + } + return component.RequiredProviderInstances() +} diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go new file mode 100644 index 0000000000..cbe3d43e6d --- /dev/null +++ b/internal/stacks/stackruntime/apply_test.go @@ -0,0 +1,244 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "encoding/json" + "fmt" + "path" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestApplyWithRemovedResource(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + attrs := map[string]interface{}{ + "id": "FE1D5830765C", + "input": map[string]interface{}{ + "value": "hello", + "type": "string", + }, + "output": map[string]interface{}{ + "value": nil, + "type": "string", + }, + "triggers_replace": nil, + } + attrsJSON, err := json.Marshal(attrs) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", "valid-providers")) + + planReq := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + + // PrevState specifies a state with a resource that is not present in + // the current configuration. This is a common situation when a resource + // is removed from the configuration but still exists in the state. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + Key: addrs.NoKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }, + Key: addrs.NoKey, + }, + }, + DeposedKey: addrs.NotDeposed, + }, + }). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: attrsJSON, + Status: states.ObjectReady, + }). + SetProviderAddr(addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), + })). + Build(), + } + + planChangesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + planResp := PlanResponse{ + PlannedChanges: planChangesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &planReq, &planResp) + planChanges, diags := collectPlanOutput(planChangesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings()) + } + + var raw []*anypb.Any + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + raw = append(raw, proto.Raw...) + } + + applyReq := ApplyRequest{ + Config: cfg, + RawPlan: raw, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + Complete: false, + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + if len(applyChanges) != 2 { + t.Fatalf("expected 2 applied changes, got %d", len(applyChanges)) + } + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: stackaddrs.AbsComponent{ + Item: stackaddrs.Component{ + Name: "self", + }, + }, + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + }, + }, + OutputValues: make(map[addrs.OutputValue]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }, + }, + }, + }, + }, + NewStateSrc: nil, // Deleted, so is nil. + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "terraform", + Namespace: "builtin", + Hostname: "terraform.io", + }, + }, + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + // An arbitrary sort just to make the result stable for comparison. + return fmt.Sprintf("%T", applyChanges[i]) < fmt.Sprintf("%T", applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func collectApplyOutput(changesCh <-chan stackstate.AppliedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + var changes []stackstate.AppliedChange + var diags tfdiags.Diagnostics + for { + select { + case change, ok := <-changesCh: + if !ok { + // The plan operation is complete but we might still have + // some buffered diagnostics to consume. + if diagsCh != nil { + for diag := range diagsCh { + diags = append(diags, diag) + } + } + return changes, diags + } + changes = append(changes, change) + case diag, ok := <-diagsCh: + if !ok { + // no more diagnostics to read + diagsCh = nil + continue + } + diags = append(diags, diag) + } + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index 3ae87192ba..e06f76f17e 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -220,8 +220,30 @@ func (c *ComponentInstance) CheckProviders(ctx context.Context, phase EvalPhase) stack := c.call.Stack(ctx) stackConfig := stack.StackConfig(ctx) declConfigs := c.call.Declaration(ctx).ProviderConfigs - neededConfigs := c.call.Config(ctx).RequiredProviderInstances(ctx) - for _, inCalleeAddr := range neededConfigs { + + // We'll iterate over the set of providers actually required for this + // operation, and make sure they have been properly declared and are + // available. + + // First, gather all the providers implied by the configuration. + configProviders := c.call.Config(ctx).RequiredProviderInstances(ctx) + + // Second, we also need to add any providers that were required by the + // component's previous runs and have since been removed from the config. + previousProviders := c.main.PreviousProviderInstances(c.Addr(), phase) + + neededProviders := configProviders.Union(previousProviders) + for _, inCalleeAddr := range neededProviders { + + providerContextString := "requires" + if !configProviders.Has(inCalleeAddr) { + // This provider was required by the previous state but is no + // longer required by the configuration, so we'll add a bit of + // extra context to the diagnostic to help the user understand + // what's going on. + providerContextString = "has resources in state that require" + } + // declConfigs is based on _local_ provider references so we'll // need to translate based on the stack configuration's // required_providers block. @@ -237,8 +259,8 @@ func (c *ComponentInstance) CheckProviders(ctx context.Context, phase EvalPhase) Severity: hcl.DiagError, Summary: "Component requires undeclared provider", Detail: fmt.Sprintf( - "The root module for %s requires a configuration for provider %q, which isn't declared as a dependency of this stack configuration.\n\nDeclare this provider in the stack's required_providers block, and then assign a configuration for that provider in this component's \"providers\" argument.", - c.Addr(), typeAddr.ForDisplay(), + "The root module for %s %s a configuration for provider %q, which isn't declared as a dependency of this stack configuration.\n\nDeclare this provider in the stack's required_providers block, and then assign a configuration for that provider in this component's \"providers\" argument.", + c.Addr(), providerContextString, typeAddr.ForDisplay(), ), Subject: c.call.Declaration(ctx).DeclRange.ToHCL().Ptr(), }) @@ -254,8 +276,8 @@ func (c *ComponentInstance) CheckProviders(ctx context.Context, phase EvalPhase) Severity: hcl.DiagError, Summary: "Missing required provider configuration", Detail: fmt.Sprintf( - "The root module for %s requires provider configuration named %q for provider %q, which is not assigned in the component's \"providers\" argument.", - c.Addr(), localAddr.StringCompact(), typeAddr.ForDisplay(), + "The root module for %s %s a provider configuration named %q for provider %q, which is not assigned in the component's \"providers\" argument.", + c.Addr(), providerContextString, localAddr.StringCompact(), typeAddr.ForDisplay(), ), Subject: c.call.Declaration(ctx).DeclRange.ToHCL().Ptr(), }) diff --git a/internal/stacks/stackruntime/internal/stackeval/main.go b/internal/stacks/stackruntime/internal/stackeval/main.go index f9665fb3ad..ff709ae3c2 100644 --- a/internal/stacks/stackruntime/internal/stackeval/main.go +++ b/internal/stacks/stackruntime/internal/stackeval/main.go @@ -418,6 +418,27 @@ func (m *Main) ProviderInstance(ctx context.Context, addr stackaddrs.AbsProvider return insts[addr.Item.Key] } +// PreviousProviderInstances fetches the set of providers that are required +// based on the current plan or state file. They are previous in the sense that +// they're not based on the current config. So if a provider has been removed +// from the config, this function will still find it. +func (m *Main) PreviousProviderInstances(addr stackaddrs.AbsComponentInstance, phase EvalPhase) addrs.Set[addrs.RootProviderConfig] { + switch phase { + case ApplyPhase: + return m.PlanBeingApplied().RequiredProviderInstances(addr) + case PlanPhase: + return m.PlanPrevState().RequiredProviderInstances(addr) + case InspectPhase: + return m.InspectingState().RequiredProviderInstances(addr) + default: + // We don't have the required information (like a plan or a state file) + // in the other phases so we can't do anything even if we wanted to. + // In general, for the other phases we're not doing anything with the + // previous provider instances anyway, so we don't need them. + return addrs.MakeSet[addrs.RootProviderConfig]() + } +} + func (m *Main) RootVariableValue(ctx context.Context, addr stackaddrs.InputVariable, phase EvalPhase) ExternalInputValue { switch phase { case PlanPhase: diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index fa01196299..864323a2e1 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -5,8 +5,11 @@ package stackruntime import ( "context" + "encoding/json" "fmt" + "path" "sort" + "strings" "testing" "time" @@ -24,6 +27,8 @@ import ( "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" @@ -614,6 +619,117 @@ func TestPlanWithProviderConfig(t *testing.T) { } }) } +func TestPlanWithRemovedResource(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + attrs := map[string]interface{}{ + "id": "FE1D5830765C", + "input": map[string]interface{}{ + "value": "hello", + "type": "string", + }, + "output": map[string]interface{}{ + "value": nil, + "type": "string", + }, + "triggers_replace": nil, + } + attrsJSON, err := json.Marshal(attrs) + if err != nil { + t.Fatal(err) + } + + // We want to see that it's adding the extra context for when a provider is + // missing for a resource that's in state and not in config. + expectedDiagnostic := "has resources in state that" + + tcs := make(map[string]*string) + tcs["missing-providers"] = &expectedDiagnostic + tcs["valid-providers"] = nil + + for name, diag := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", name)) + + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + + // PrevState specifies a state with a resource that is not present in + // the current configuration. This is a common situation when a resource + // is removed from the configuration but still exists in the state. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + Key: addrs.NoKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }, + Key: addrs.NoKey, + }, + }, + DeposedKey: addrs.NotDeposed, + }, + }). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: attrsJSON, + Status: states.ObjectReady, + }). + SetProviderAddr(addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), + })). + Build(), + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + _, diags := collectPlanOutput(changesCh, diagsCh) + + if diag != nil { + if len(diags) == 0 { + t.Fatalf("expected diagnostics, got none") + } + if !strings.Contains(diags[0].Description().Detail, *diag) { + t.Fatalf("expected diagnostic %q, got %q", *diag, diags[0].Description().Detail) + } + } else if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error()) + } + }) + } +} // collectPlanOutput consumes the two output channels emitting results from // a call to [Plan], and collects all of the data written to them before diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf new file mode 100644 index 0000000000..3b77397fa0 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl new file mode 100644 index 0000000000..48e5e4959a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl @@ -0,0 +1,13 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" {} + +component "self" { + source = "../" + + providers = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl new file mode 100644 index 0000000000..5355ab8efb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl @@ -0,0 +1,15 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" {} + +component "self" { + source = "../" + + providers = { + terraform = provider.terraform.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf new file mode 100644 index 0000000000..b89447e101 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} + +resource "terraform_data" "main" { + input = "hello" +} + +output "input" { + value = terraform_data.main.input +} + +output "output" { + value = terraform_data.main.output +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl new file mode 100644 index 0000000000..2d3a30d087 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl @@ -0,0 +1,22 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" { +} + +component "self" { + source = "./" + + providers = {} +} + +output "obj" { + type = object({ + input = string + output = string + }) + value = component.self +} diff --git a/internal/stacks/stackstate/state.go b/internal/stacks/stackstate/state.go index 9b9293cce1..49799a89be 100644 --- a/internal/stacks/stackstate/state.go +++ b/internal/stacks/stackstate/state.go @@ -4,12 +4,13 @@ package stackstate import ( + "google.golang.org/protobuf/types/known/anypb" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" "github.com/hashicorp/terraform/internal/states" - "google.golang.org/protobuf/types/known/anypb" ) // State represents a previous run's state snapshot. @@ -118,6 +119,30 @@ func (s *State) ResourceInstanceObjectSrc(addr stackaddrs.AbsResourceInstanceObj return rios.src } +// RequiredProviderInstances returns a description of all of the provider +// instance slots that are required to satisfy the resource instances +// belonging to the given component instance. +// +// See also stackeval.ComponentConfig.RequiredProviderInstances for a similar +// function that operates on the configuration of a component instance rather +// than the state of one. +func (s *State) RequiredProviderInstances(component stackaddrs.AbsComponentInstance) addrs.Set[addrs.RootProviderConfig] { + state, ok := s.componentInstances.GetOk(component) + if !ok { + // Then we have no state for this component, which is fine. + return addrs.MakeSet[addrs.RootProviderConfig]() + } + + providerInstances := addrs.MakeSet[addrs.RootProviderConfig]() + for _, elem := range state.resourceInstanceObjects.Elems { + providerInstances.Add(addrs.RootProviderConfig{ + Provider: elem.Value.providerConfigAddr.Provider, + Alias: elem.Value.providerConfigAddr.Alias, + }) + } + return providerInstances +} + func (s *State) resourceInstanceObjectState(addr stackaddrs.AbsResourceInstanceObject) *resourceInstanceObjectState { cs, ok := s.componentInstances.GetOk(addr.Component) if !ok { diff --git a/internal/stacks/stackstate/state_builder.go b/internal/stacks/stackstate/state_builder.go new file mode 100644 index 0000000000..31d96c1c6e --- /dev/null +++ b/internal/stacks/stackstate/state_builder.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +// StateBuilder wraps State, and provides some write-only methods to update the +// state. +// +// This is generally used to build up a new state from scratch during tests. +type StateBuilder struct { + state *State +} + +func NewStateBuilder() *StateBuilder { + return &StateBuilder{ + state: NewState(), + } +} + +// Build returns the state and invalidates the StateBuilder. +// +// You will get nil pointer exceptions if you attempt to use the builder after +// calling Build. +func (s *StateBuilder) Build() *State { + ret := s.state + s.state = nil + return ret +} + +// AddResourceInstance adds a resource instance to the state. +func (s *StateBuilder) AddResourceInstance(builder *ResourceInstanceBuilder) *StateBuilder { + if builder.addr == nil || builder.src == nil || builder.providerAddr == nil { + panic("ResourceInstanceBuilder is missing required fields") + } + s.state.addResourceInstanceObject(*builder.addr, builder.src, *builder.providerAddr) + return s +} + +type ResourceInstanceBuilder struct { + addr *stackaddrs.AbsResourceInstanceObject + src *states.ResourceInstanceObjectSrc + providerAddr *addrs.AbsProviderConfig +} + +func NewResourceInstanceBuilder() *ResourceInstanceBuilder { + return &ResourceInstanceBuilder{} +} + +func (b *ResourceInstanceBuilder) SetAddr(addr stackaddrs.AbsResourceInstanceObject) *ResourceInstanceBuilder { + b.addr = &addr + return b +} + +func (b *ResourceInstanceBuilder) SetResourceInstanceObjectSrc(src states.ResourceInstanceObjectSrc) *ResourceInstanceBuilder { + b.src = &src + return b +} + +func (b *ResourceInstanceBuilder) SetProviderAddr(addr addrs.AbsProviderConfig) *ResourceInstanceBuilder { + b.providerAddr = &addr + return b +}