Skip to content

Commit

Permalink
Add support for scoped resources
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcervante committed Feb 20, 2023
1 parent 71774c2 commit d0a15b7
Show file tree
Hide file tree
Showing 20 changed files with 78 additions and 37 deletions.
16 changes: 16 additions & 0 deletions 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
}
7 changes: 7 additions & 0 deletions internal/configs/resource.go
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion internal/lang/data.go
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/lang/data_test.go
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion internal/lang/eval.go
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions internal/lang/scope.go
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/context_eval.go
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/terraform/eval_conditions.go
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/eval_context.go
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions internal/terraform/eval_context_builtin.go
Expand Up @@ -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)
Expand All @@ -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)
}

Expand Down Expand Up @@ -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")
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/eval_context_mock.go
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/eval_for_each.go
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/eval_variable.go
Expand Up @@ -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 {
Expand Down
11 changes: 6 additions & 5 deletions internal/terraform/evaluate.go
Expand Up @@ -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.
}
}

Expand Down
16 changes: 8 additions & 8 deletions internal/terraform/evaluate_test.go
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
21 changes: 15 additions & 6 deletions internal/terraform/evaluate_valid.go
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/evaluate_valid_test.go
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/node_module_variable.go
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/node_resource_abstract_instance.go
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions internal/terraform/node_resource_validate.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit d0a15b7

Please sign in to comment.