Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/addrs/module_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ func (c ModuleCall) Equal(other ModuleCall) bool {
return c.Name == other.Name
}

func (c ModuleCall) Output(name string) ModuleCallOutput {
return ModuleCallOutput{
Call: c,
Name: name,
}
}

// AbsModuleCall is the address of a "module" block relative to the root
// of the configuration.
//
Expand Down Expand Up @@ -176,6 +183,15 @@ func (m ModuleCallOutput) UniqueKey() UniqueKey {

func (m ModuleCallOutput) uniqueKeySigil() {}

func (m ModuleCallOutput) ConfigOutputValue(baseModule Module) ConfigOutputValue {
return ConfigOutputValue{
Module: baseModule.Child(m.Call.Name),
OutputValue: OutputValue{
Name: m.Name,
},
}
}

// ModuleCallInstanceOutput is the address of a particular named output produced by
// an instance of a module call.
type ModuleCallInstanceOutput struct {
Expand Down
11 changes: 11 additions & 0 deletions internal/configs/named_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,14 @@ type Output struct {
DependsOn []hcl.Traversal
Sensitive bool
Ephemeral bool
Deprecated string

Preconditions []*CheckRule

DescriptionSet bool
SensitiveSet bool
EphemeralSet bool
DeprecatedSet bool

DeclRange hcl.Range
}
Expand Down Expand Up @@ -402,6 +404,12 @@ func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostic
o.EphemeralSet = true
}

if attr, exists := content.Attributes["deprecated"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Deprecated)
diags = append(diags, valDiags...)
o.DeprecatedSet = true
}

if attr, exists := content.Attributes["depends_on"]; exists {
deps, depsDiags := DecodeDependsOn(attr)
diags = append(diags, depsDiags...)
Expand Down Expand Up @@ -525,6 +533,9 @@ var outputBlockSchema = &hcl.BodySchema{
{
Name: "ephemeral",
},
{
Name: "deprecated",
},
},
Blocks: []hcl.BlockHeaderSchema{
{Type: "precondition"},
Expand Down
27 changes: 27 additions & 0 deletions internal/configs/named_values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,30 @@ func TestVariableInvalidDefault(t *testing.T) {
}
}
}

func TestOutputDeprecation(t *testing.T) {
src := `
output "foo" {
value = "bar"
deprecated = "This output is deprecated"
}
`

hclF, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos)
if diags.HasErrors() {
t.Fatal(diags.Error())
}

b, diags := parseConfigFile(hclF.Body, nil, false, false)
if diags.HasErrors() {
t.Fatalf("unexpected error: %q", diags)
}

if !b.Outputs[0].DeprecatedSet {
t.Fatalf("expected output to be deprecated")
}

if b.Outputs[0].Deprecated != "This output is deprecated" {
t.Fatalf("expected output to have deprecation message")
}
}
106 changes: 106 additions & 0 deletions internal/terraform/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import (

"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
Expand Down Expand Up @@ -185,6 +187,110 @@ func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas
return ret, diags
}

type Deprecation struct {
Message string
Range hcl.Range
}

func (c *Context) ValidateDeprecation(config *configs.Config) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics

// This will be inefficient since we
// a) could be walking the module tree in a way that also continiously validates
// b) build the same deprecated output map multiple times
// I am ok with inefficiency since this is a POC

// Depth-first search for all deprecated outputs (build abs addrs)

deprecatedOutputs := addrs.MakeMap[addrs.ConfigOutputValue, Deprecation]()
// We walk through all module calls to find deprecated outputs that can potentially be referenced in this module
for _, child := range config.Children {
childDiags := c.ValidateDeprecation(child)
diags = diags.Append(childDiags)

for _, output := range child.Module.Outputs {
if output.DeprecatedSet {
deprecatedOutputs.Put(addrs.ConfigOutputValue{
Module: child.Path,
OutputValue: addrs.OutputValue{
Name: output.Name,
},
}, Deprecation{
Message: output.Deprecated,
Range: output.DeclRange, // TODO: Maybe make this a range for the deprecation?
})
}
}
}

// Check if any deprecated outputs are used in the config
// TODO: Loop over every top-level block
for _, resource := range config.Module.ManagedResources {
// TODO: This would need to take provider fields into account
schema, err := c.plugins.ResourceTypeSchema(addrs.ImpliedProviderForUnqualifiedType(resource.Addr().ImpliedProvider()), addrs.ManagedResourceMode, resource.Type)
if err != nil {
panic(err.Error())
}
// TODO: Ignoring diags for now
bodyRefs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, resource.Config, schema.Body)
forEachRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, resource.ForEach)
countRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, resource.Count)
refs := append(bodyRefs, forEachRefs...)
refs = append(refs, countRefs...)
diags = diags.Append(diagsForDeprecatedRefs(deprecatedOutputs, config.Path, refs))
}

for _, output := range config.Module.Outputs {
if !output.DeprecatedSet {
refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, output.Expr)
diags = diags.Append(diagsForDeprecatedRefs(deprecatedOutputs, config.Path, refs))
}
}

return diags
}

func diagsForDeprecatedRefs(deprecatedOutputs addrs.Map[addrs.ConfigOutputValue, Deprecation], modulePath addrs.Module, refs []*addrs.Reference) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, ref := range refs {
switch r := ref.Subject.(type) {
case addrs.ModuleCallInstanceOutput:
// Check if this references a deprecated output
searchConfigOutputValue := r.ModuleCallOutput().ConfigOutputValue(modulePath)
if deprecation, ok := deprecatedOutputs.GetOk(searchConfigOutputValue); ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecated value used",
Detail: deprecation.Message,
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}

// This case is used when e.g. a splat expression is encountered
// We then need to parse the remainder, we know it needs to be an output name
case addrs.ModuleCall:
var outputName string
fmt.Printf("\n\t ref --> %#v\n", ref)
fmt.Printf("\n\t ref.Remaining --> %#v\n", ref.Remaining)

searchConfigOutputValue := r.Output(outputName).ConfigOutputValue(modulePath)
if deprecation, ok := deprecatedOutputs.GetOk(searchConfigOutputValue); ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecated value used",
Detail: deprecation.Message,
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}

default:
fmt.Printf("\n\t r --> %#v\n", r)
continue
}
}
return diags
}

type ContextGraphOpts struct {
// If false, skip the graph structure validation.
SkipGraphValidation bool
Expand Down
6 changes: 6 additions & 0 deletions internal/terraform/context_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ func (c *Context) Validate(config *configs.Config, opts *ValidateOpts) tfdiags.D
return diags
}

moreDiags = c.ValidateDeprecation(config)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}

log.Printf("[DEBUG] Building and walking validate graph")

// Validate is to check if the given module is valid regardless of
Expand Down
Loading
Loading