From 68fd15dfddb5ca57b787dfb687e7b6bf525e557a Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Mon, 26 Feb 2024 11:26:22 +0100 Subject: [PATCH] stacks: expand reference scope of static validations (#34721) --- .../internal/stackeval/component_config.go | 17 ++- .../internal/stackeval/provider_config.go | 32 +++++- .../internal/stackeval/stack_call_config.go | 22 +++- .../internal/stackeval/stack_config.go | 106 +++++++++++++++++- 4 files changed, 167 insertions(+), 10 deletions(-) diff --git a/internal/stacks/stackruntime/internal/stackeval/component_config.go b/internal/stacks/stackruntime/internal/stackeval/component_config.go index 9771d0cb33..623981ec17 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_config.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/promising" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" @@ -55,6 +56,10 @@ func (c *ComponentConfig) Declaration(ctx context.Context) *stackconfig.Componen return c.config } +func (c *ComponentConfig) StackConfig(ctx context.Context) *StackConfig { + return c.main.mustStackConfig(ctx, c.addr.Stack) +} + // ModuleTree returns the static representation of the tree of modules starting // at the component's configured source address, or nil if any of the // modules have errors that prevent even static decoding. @@ -285,7 +290,7 @@ func (c *ComponentConfig) RequiredProviderInstances(ctx context.Context) addrs.S func (c *ComponentConfig) CheckProviders(ctx context.Context, phase EvalPhase) (addrs.Set[addrs.RootProviderConfig], tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - stackConfig := c.main.StackConfig(ctx, c.Addr().Stack) + stackConfig := c.StackConfig(ctx) declConfigs := c.Declaration(ctx).ProviderConfigs neededProviders := c.RequiredProviderInstances(ctx) @@ -408,6 +413,16 @@ func (c *ComponentConfig) ExprReferenceValue(ctx context.Context, phase EvalPhas return cty.DynamicVal } +func (c *ComponentConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + repetition := instances.RepetitionData{} + if c.Declaration(ctx).ForEach != nil { + // For validation, we'll return unknown for the instance data. + repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() + repetition.EachValue = cty.DynamicVal + } + return c.StackConfig(ctx).resolveExpressionReference(ctx, ref, nil, repetition) +} + func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { diags, err := c.validate.Do(ctx, func(ctx context.Context) (tfdiags.Diagnostics, error) { var diags tfdiags.Diagnostics diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config.go b/internal/stacks/stackruntime/internal/stackeval/provider_config.go index e0af751875..9fbc6e6e03 100644 --- a/internal/stacks/stackruntime/internal/stackeval/provider_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config.go @@ -10,6 +10,8 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/promising" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform/internal/stacks/stackconfig" "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // ProviderConfig represents a single "provider" block in a stack configuration. @@ -147,9 +148,34 @@ func (p *ProviderConfig) ResolveExpressionReference(ctx context.Context, ref sta repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() repetition.EachValue = cty.DynamicVal } - return p.main. + ret, diags := p.main. mustStackConfig(ctx, p.Addr().Stack). - resolveExpressionReference(ctx, ref, repetition, nil) + resolveExpressionReference(ctx, ref, nil, repetition) + + if _, ok := ret.(*ProviderConfig); ok { + // We can't reference other providers from anywhere inside a provider + // configuration block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", ref.Target.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + + return ret, diags +} + +// ExprReferenceValue implements Referenceable. +func (p *ProviderConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + // We don't say anything about the contents of a provider during the + // static evaluation phase. We still return the type of the provider so + // we can use it to verify type constraints, but we don't return any + // actual values. + if p.config.ForEach != nil { + return cty.UnknownVal(cty.Map(p.InstRefValueType(ctx))) + } + return cty.UnknownVal(p.InstRefValueType(ctx)) } var providerInstanceRefTypes = map[addrs.Provider]cty.Type{} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go index e05aa9cfae..4d4326f676 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go @@ -8,14 +8,15 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/promising" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackconfig" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" ) // StackCallConfig represents a "stack" block in a stack configuration, @@ -320,9 +321,22 @@ func (s *StackCallConfig) ResolveExpressionReference(ctx context.Context, ref st repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() repetition.EachValue = cty.DynamicVal } - return s.main. + ret, diags := s.main. mustStackConfig(ctx, s.Addr().Stack). - resolveExpressionReference(ctx, ref, instances.RepetitionData{}, nil) + resolveExpressionReference(ctx, ref, nil, repetition) + + if _, ok := ret.(*ProviderConfig); ok { + // We can't reference other providers from anywhere inside an embedded + // stack call - they should define their own providers. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", ref.Target.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + + return ret, diags } func (s *StackCallConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_config.go index 29ad27f412..c92f74a260 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack_config.go @@ -224,6 +224,42 @@ func (s *StackConfig) Provider(ctx context.Context, addr stackaddrs.ProviderConf return ret } +// ProviderByLocalAddr returns a [ProviderConfig] representing the provider +// configuration block within the stack configuration that matches the given +// local address, or nil if there is no such declaration. +// +// This is equivalent to calling [Provider] just using a reference address +// instead of a config address. +func (s *StackConfig) ProviderByLocalAddr(ctx context.Context, localAddr stackaddrs.ProviderConfigRef) *ProviderConfig { + s.mu.Lock() + defer s.mu.Unlock() + + provider, ok := s.config.Stack.RequiredProviders.ProviderForLocalName(localAddr.ProviderLocalName) + if !ok { + return nil + } + + addr := stackaddrs.ProviderConfig{ + Provider: provider, + Name: localAddr.Name, + } + ret, ok := s.providers[addr] + if !ok { + configAddr := addrs.LocalProviderConfig{ + LocalName: localAddr.ProviderLocalName, + Alias: localAddr.Name, + } + cfg, ok := s.config.Stack.ProviderConfigs[configAddr] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.Addr(), addr) + ret = newProviderConfig(s.main, cfgAddr, cfg) + s.providers[addr] = ret + } + return ret +} + // ProviderLocalName returns the local name used for the given provider // in this particular stack configuration, based on the declarations in // the required_providers configuration block. @@ -315,13 +351,13 @@ func (s *StackConfig) Components(ctx context.Context) map[stackaddrs.Component]* // global scope for evaluation within an unexpanded stack during the validate // phase. func (s *StackConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { - return s.resolveExpressionReference(ctx, ref, instances.RepetitionData{}, nil) + return s.resolveExpressionReference(ctx, ref, nil, instances.RepetitionData{}) } // resolveExpressionReference is the shared implementation of various // validation-time ResolveExpressionReference methods, factoring out all // of the common parts into one place. -func (s *StackConfig) resolveExpressionReference(ctx context.Context, ref stackaddrs.Reference, repetition instances.RepetitionData, selfAddr stackaddrs.Referenceable) (Referenceable, tfdiags.Diagnostics) { +func (s *StackConfig) resolveExpressionReference(ctx context.Context, ref stackaddrs.Reference, selfAddr stackaddrs.Referenceable, repetition instances.RepetitionData) (Referenceable, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // "Test-only globals" is a special affordance we have only when running @@ -371,6 +407,72 @@ func (s *StackConfig) resolveExpressionReference(ctx context.Context, ref stacka return nil, diags } return ret, diags + case stackaddrs.ProviderConfigRef: + ret := s.ProviderByLocalAddr(ctx, addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared provider configuration", + Detail: fmt.Sprintf("There is no provider %q %q block declared this stack.", addr.ProviderLocalName, addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.ContextualRef: + switch addr { + case stackaddrs.EachKey: + if repetition.EachKey == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'each' reference", + Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.EachKey}, diags + case stackaddrs.EachValue: + if repetition.EachValue == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'each' reference", + Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.EachValue}, diags + case stackaddrs.CountIndex: + if repetition.CountIndex == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'count' reference", + Detail: "The special symbol 'count' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'count' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.CountIndex}, diags + case stackaddrs.Self: + if selfAddr != nil { + // We'll just pretend the reference was to whatever "self" + // is referring to, then. + ref.Target = selfAddr + return s.resolveExpressionReference(ctx, ref, nil, repetition) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'self' reference", + Detail: "The special symbol 'self' is not defined in this location.", + Context: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + default: + // The above should be exhaustive for all defined values of this type. + panic(fmt.Sprintf("unsupported ContextualRef %#v", addr)) + } default: diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError,