From af05cbb645c2b09edd8e49098988e56c322a3017 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Fri, 10 Mar 2023 11:11:10 +0100 Subject: [PATCH] Add support for scoped resources (#32732) --- internal/configs/container.go | 16 ++++++++++++++ internal/configs/resource.go | 7 +++++++ internal/lang/data.go | 2 +- internal/lang/data_test.go | 2 +- internal/lang/eval.go | 2 +- internal/lang/scope.go | 8 +++++++ internal/terraform/context_eval.go | 2 +- internal/terraform/eval_conditions.go | 2 +- internal/terraform/eval_context.go | 2 +- internal/terraform/eval_context_builtin.go | 8 +++---- internal/terraform/eval_context_mock.go | 2 +- internal/terraform/eval_for_each.go | 2 +- internal/terraform/eval_variable.go | 2 +- internal/terraform/evaluate.go | 11 +++++----- internal/terraform/evaluate_test.go | 16 +++++++------- internal/terraform/evaluate_valid.go | 21 +++++++++++++------ internal/terraform/evaluate_valid_test.go | 2 +- internal/terraform/node_module_variable.go | 2 +- .../node_resource_abstract_instance.go | 2 +- internal/terraform/node_resource_validate.go | 4 ++-- 20 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 internal/configs/container.go diff --git a/internal/configs/container.go b/internal/configs/container.go new file mode 100644 index 0000000000..b43c2c17a2 --- /dev/null +++ b/internal/configs/container.go @@ -0,0 +1,16 @@ +package configs + +import "github.com/hashicorp/terraform/internal/addrs" + +// Container provides an interface for scoped resources. +// +// Any resources contained within a Container should not be accessible from +// outside the container. +type Container interface { + // Accessible should return true if the resource specified by addr can + // reference other items within this Container. + // + // Typically, that means that addr will either be the container itself or + // something within the container. + Accessible(addr addrs.Referenceable) bool +} diff --git a/internal/configs/resource.go b/internal/configs/resource.go index 1f67c6c40f..f0d809ab1f 100644 --- a/internal/configs/resource.go +++ b/internal/configs/resource.go @@ -37,6 +37,13 @@ type Resource struct { // For all other resource modes, this field is nil. Managed *ManagedResource + // Container links a scoped resource back up to the resources that contains + // it. This field is referenced during static analysis to check whether any + // references are also made from within the same container. + // + // If this is nil, then this resource is essentially public. + Container Container + DeclRange hcl.Range TypeRange hcl.Range } diff --git a/internal/lang/data.go b/internal/lang/data.go index 710fccedc8..f79298e03b 100644 --- a/internal/lang/data.go +++ b/internal/lang/data.go @@ -20,7 +20,7 @@ import ( // cases where it's not possible to even determine a suitable result type, // cty.DynamicVal is returned along with errors describing the problem. type Data interface { - StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics + StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics GetCountAttr(addrs.CountAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) GetForEachAttr(addrs.ForEachAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) diff --git a/internal/lang/data_test.go b/internal/lang/data_test.go index e86a856183..010c243fa9 100644 --- a/internal/lang/data_test.go +++ b/internal/lang/data_test.go @@ -19,7 +19,7 @@ type dataForTests struct { var _ Data = &dataForTests{} -func (d *dataForTests) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics { +func (d *dataForTests) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { return nil // does nothing in this stub implementation } diff --git a/internal/lang/eval.go b/internal/lang/eval.go index 5c82392bcc..f25398e7c9 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -259,7 +259,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl // First we'll do static validation of the references. This catches things // early that might otherwise not get caught due to unknown values being // present in the scope during planning. - staticDiags := s.Data.StaticValidateReferences(refs, selfAddr) + staticDiags := s.Data.StaticValidateReferences(refs, selfAddr, s.SourceAddr) diags = diags.Append(staticDiags) if staticDiags.HasErrors() { return ctx, diags diff --git a/internal/lang/scope.go b/internal/lang/scope.go index 6c229e25d9..fea113269b 100644 --- a/internal/lang/scope.go +++ b/internal/lang/scope.go @@ -20,6 +20,14 @@ type Scope struct { // or nil if the "self" object should not be available at all. SelfAddr addrs.Referenceable + // SourceAddr is the address of the source item for the scope. This will + // affect any scoped resources that can be accessed from within this scope. + // + // If nil, access is assumed to be at the module level. So, in practice this + // only needs to be set for items that should be able to access something + // hidden in their own scope. + SourceAddr addrs.Referenceable + // BaseDir is the base directory used by any interpolation functions that // accept filesystem paths as arguments. BaseDir string diff --git a/internal/terraform/context_eval.go b/internal/terraform/context_eval.go index f9d0f64933..29875bec38 100644 --- a/internal/terraform/context_eval.go +++ b/internal/terraform/context_eval.go @@ -92,5 +92,5 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a // caches its contexts, so we should get hold of the context that was // previously used for evaluation here, unless we skipped walking. evalCtx := walker.EnterPath(moduleAddr) - return evalCtx.EvaluationScope(nil, EvalDataForNoInstanceKey), diags + return evalCtx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey), diags } diff --git a/internal/terraform/eval_conditions.go b/internal/terraform/eval_conditions.go index 58877011ce..3f2e3352fb 100644 --- a/internal/terraform/eval_conditions.go +++ b/internal/terraform/eval_conditions.go @@ -87,7 +87,7 @@ func evalCheckRule(typ addrs.CheckType, rule *configs.CheckRule, ctx EvalContext panic(fmt.Sprintf("Invalid self reference type %t", self)) } } - scope := ctx.EvaluationScope(selfReference, keyData) + scope := ctx.EvaluationScope(selfReference, nil, keyData) hclCtx, moreDiags := scope.EvalContext(refs) diags = diags.Append(moreDiags) diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index fedf223051..61320ef32f 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -125,7 +125,7 @@ type EvalContext interface { // EvaluationScope returns a scope that can be used to evaluate reference // addresses in this context. - EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope + EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope // SetRootModuleArgument defines the value for one variable of the root // module. The caller must ensure that given value is a suitable diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index d66b9dd080..2953e008b6 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -270,7 +270,7 @@ func (ctx *BuiltinEvalContext) CloseProvisioners() error { func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - scope := ctx.EvaluationScope(self, keyData) + scope := ctx.EvaluationScope(self, nil, keyData) body, evalDiags := scope.ExpandBlock(body, schema) diags = diags.Append(evalDiags) val, evalDiags := scope.EvalBlock(body, schema) @@ -279,7 +279,7 @@ func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema } func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) { - scope := ctx.EvaluationScope(self, EvalDataForNoInstanceKey) + scope := ctx.EvaluationScope(self, nil, EvalDataForNoInstanceKey) return scope.EvalExpr(expr, wantType) } @@ -397,7 +397,7 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r return ref, replace, diags } -func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData instances.RepetitionData) *lang.Scope { +func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { if !ctx.pathSet { panic("context path not set") } @@ -407,7 +407,7 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData InstanceKeyData: keyData, Operation: ctx.Evaluator.Operation, } - scope := ctx.Evaluator.Scope(data, self) + scope := ctx.Evaluator.Scope(data, self, source) // ctx.PathValue is the path of the module that contains whatever // expression the caller will be trying to evaluate, so this will diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index 24159ef955..23f0ea8648 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -319,7 +319,7 @@ func (c *MockEvalContext) installSimpleEval() { } } -func (c *MockEvalContext) EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { +func (c *MockEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { c.EvaluationScopeCalled = true c.EvaluationScopeSelf = self c.EvaluationScopeKeyData = keyData diff --git a/internal/terraform/eval_for_each.go b/internal/terraform/eval_for_each.go index 3c80ebff01..9e89439a31 100644 --- a/internal/terraform/eval_for_each.go +++ b/internal/terraform/eval_for_each.go @@ -43,7 +43,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU refs, moreDiags := lang.ReferencesInExpr(expr) diags = diags.Append(moreDiags) - scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) + scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) var hclCtx *hcl.EvalContext if scope != nil { hclCtx, moreDiags = scope.EvalContext(refs) diff --git a/internal/terraform/eval_variable.go b/internal/terraform/eval_variable.go index 8878886383..bd98698a1b 100644 --- a/internal/terraform/eval_variable.go +++ b/internal/terraform/eval_variable.go @@ -223,7 +223,7 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config config.Name: val, }), }, - Functions: ctx.EvaluationScope(nil, EvalDataForNoInstanceKey).Functions(), + Functions: ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey).Functions(), } for _, validation := range config.Validations { diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index d680136d8d..0beb29b21b 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -68,12 +68,13 @@ type Evaluator struct { // If the "self" argument is nil then the "self" object is not available // in evaluated expressions. Otherwise, it behaves as an alias for the given // address. -func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable) *lang.Scope { +func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable) *lang.Scope { return &lang.Scope{ - Data: data, - SelfAddr: self, - PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval, - BaseDir: ".", // Always current working directory for now. + Data: data, + SelfAddr: self, + SourceAddr: source, + PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval, + BaseDir: ".", // Always current working directory for now. } } diff --git a/internal/terraform/evaluate_test.go b/internal/terraform/evaluate_test.go index 765efded68..87ab4ca73c 100644 --- a/internal/terraform/evaluate_test.go +++ b/internal/terraform/evaluate_test.go @@ -25,7 +25,7 @@ func TestEvaluatorGetTerraformAttr(t *testing.T) { data := &evaluationStateData{ Evaluator: evaluator, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil) t.Run("workspace", func(t *testing.T) { want := cty.StringVal("foo") @@ -55,7 +55,7 @@ func TestEvaluatorGetPathAttr(t *testing.T) { data := &evaluationStateData{ Evaluator: evaluator, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil) t.Run("module", func(t *testing.T) { want := cty.StringVal("bar/baz") @@ -124,7 +124,7 @@ func TestEvaluatorGetInputVariable(t *testing.T) { data := &evaluationStateData{ Evaluator: evaluator, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil) want := cty.StringVal("bar").Mark(marks.Sensitive) got, diags := scope.Data.GetInputVariable(addrs.InputVariable{ @@ -273,7 +273,7 @@ func TestEvaluatorGetResource(t *testing.T) { data := &evaluationStateData{ Evaluator: evaluator, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil) want := cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), @@ -438,7 +438,7 @@ func TestEvaluatorGetResource_changes(t *testing.T) { data := &evaluationStateData{ Evaluator: evaluator, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil) want := cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), @@ -473,7 +473,7 @@ func TestEvaluatorGetModule(t *testing.T) { data := &evaluationStateData{ Evaluator: evaluator, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil) want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)}) got, diags := scope.Data.GetModule(addrs.ModuleCall{ Name: "mod", @@ -501,7 +501,7 @@ func TestEvaluatorGetModule(t *testing.T) { data = &evaluationStateData{ Evaluator: evaluator, } - scope = evaluator.Scope(data, nil) + scope = evaluator.Scope(data, nil, nil) want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)}) got, diags = scope.Data.GetModule(addrs.ModuleCall{ Name: "mod", @@ -519,7 +519,7 @@ func TestEvaluatorGetModule(t *testing.T) { data = &evaluationStateData{ Evaluator: evaluator, } - scope = evaluator.Scope(data, nil) + scope = evaluator.Scope(data, nil, nil) want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)}) got, diags = scope.Data.GetModule(addrs.ModuleCall{ Name: "mod", diff --git a/internal/terraform/evaluate_valid.go b/internal/terraform/evaluate_valid.go index 1d43cc4fce..30331a756a 100644 --- a/internal/terraform/evaluate_valid.go +++ b/internal/terraform/evaluate_valid.go @@ -28,16 +28,16 @@ import ( // // The result may include warning diagnostics if, for example, deprecated // features are referenced. -func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics { +func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { var diags tfdiags.Diagnostics for _, ref := range refs { - moreDiags := d.staticValidateReference(ref, self) + moreDiags := d.staticValidateReference(ref, self, source) diags = diags.Append(moreDiags) } return diags } -func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics { +func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { modCfg := d.Evaluator.Config.DescendentForInstance(d.ModulePath) if modCfg == nil { // This is a bug in the caller rather than a problem with the @@ -78,12 +78,12 @@ func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self case addrs.Resource: var diags tfdiags.Diagnostics diags = diags.Append(d.staticValidateSingleResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) - diags = diags.Append(d.staticValidateResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) + diags = diags.Append(d.staticValidateResourceReference(modCfg, addr, source, ref.Remaining, ref.SourceRange)) return diags case addrs.ResourceInstance: var diags tfdiags.Diagnostics diags = diags.Append(d.staticValidateMultiResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) - diags = diags.Append(d.staticValidateResourceReference(modCfg, addr.ContainingResource(), ref.Remaining, ref.SourceRange)) + diags = diags.Append(d.staticValidateResourceReference(modCfg, addr.ContainingResource(), source, ref.Remaining, ref.SourceRange)) return diags // We also handle all module call references the same way, disregarding index. @@ -187,7 +187,7 @@ func (d *evaluationStateData) staticValidateMultiResourceReference(modCfg *confi return diags } -func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { +func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource, source addrs.Referenceable, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { var diags tfdiags.Diagnostics var modeAdjective string @@ -223,6 +223,15 @@ func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Co return diags } + if cfg.Container != nil && (source == nil || !cfg.Container.Accessible(source)) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to scoped resource`, + Detail: fmt.Sprintf(`The referenced %s resource %q %q is not available from this context.`, modeAdjective, addr.Type, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + } + providerFqn := modCfg.Module.ProviderForLocalConfig(cfg.ProviderConfigAddr()) schema, _, err := d.Evaluator.Plugins.ResourceTypeSchema(providerFqn, addr.Mode, addr.Type) if err != nil { diff --git a/internal/terraform/evaluate_valid_test.go b/internal/terraform/evaluate_valid_test.go index cfdfdea1f5..920bdbef2d 100644 --- a/internal/terraform/evaluate_valid_test.go +++ b/internal/terraform/evaluate_valid_test.go @@ -100,7 +100,7 @@ For example, to correlate with indices of a referring resource, use: Evaluator: evaluator, } - diags = data.StaticValidateReferences(refs, nil) + diags = data.StaticValidateReferences(refs, nil, nil) if diags.HasErrors() { if test.WantErr == "" { t.Fatalf("Unexpected diagnostics: %s", diags.Err()) diff --git a/internal/terraform/node_module_variable.go b/internal/terraform/node_module_variable.go index 6d5ae2af89..4295298086 100644 --- a/internal/terraform/node_module_variable.go +++ b/internal/terraform/node_module_variable.go @@ -214,7 +214,7 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo moduleInstanceRepetitionData = ctx.InstanceExpander().GetModuleInstanceRepetitionData(n.ModuleInstance) } - scope := ctx.EvaluationScope(nil, moduleInstanceRepetitionData) + scope := ctx.EvaluationScope(nil, nil, moduleInstanceRepetitionData) val, moreDiags := scope.EvalExpr(expr, cty.DynamicPseudoType) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 4dabb7bedb..709298ee51 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -2049,7 +2049,7 @@ func (n *NodeAbstractResourceInstance) evalDestroyProvisionerConfig(ctx EvalCont // destroy-time provisioners. keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, nil) - evalScope := ctx.EvaluationScope(n.ResourceInstanceAddr().Resource, keyData) + evalScope := ctx.EvaluationScope(n.ResourceInstanceAddr().Resource, nil, keyData) config, evalDiags := evalScope.EvalSelfBlock(body, self, schema, keyData) diags = diags.Append(evalDiags) diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index a70bcdc4bc..b0ea8c70a1 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -465,7 +465,7 @@ func (n *NodeValidatableResource) evaluateExpr(ctx EvalContext, expr hcl.Express refs, refDiags := lang.ReferencesInExpr(expr) diags = diags.Append(refDiags) - scope := ctx.EvaluationScope(self, keyData) + scope := ctx.EvaluationScope(self, nil, keyData) hclCtx, moreDiags := scope.EvalContext(refs) diags = diags.Append(moreDiags) @@ -581,7 +581,7 @@ func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiag // we'll just eval it and count on the fact that our evaluator will // detect references to non-existent objects. if !diags.HasErrors() { - scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) + scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) if scope != nil { // sometimes nil in tests, due to incomplete mocks _, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType) diags = diags.Append(refDiags)