From a33218b4946663c577a4a927ab53b5c7e1e38048 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Fri, 15 Nov 2024 11:53:18 +0100 Subject: [PATCH 1/3] Add new deprecated attribute to output blocks --- internal/configs/named_values.go | 11 +++++++++++ internal/configs/named_values_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go index c9c92e45e074..35e371facd29 100644 --- a/internal/configs/named_values.go +++ b/internal/configs/named_values.go @@ -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 } @@ -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...) @@ -525,6 +533,9 @@ var outputBlockSchema = &hcl.BodySchema{ { Name: "ephemeral", }, + { + Name: "deprecated", + }, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "precondition"}, diff --git a/internal/configs/named_values_test.go b/internal/configs/named_values_test.go index 0626157c031e..5190986d5e4e 100644 --- a/internal/configs/named_values_test.go +++ b/internal/configs/named_values_test.go @@ -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") + } +} From 192ace856e6ecff27958e0c88557d8ccbffcf517 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Fri, 15 Nov 2024 11:58:27 +0100 Subject: [PATCH 2/3] Track output dependants in an extra graph transformer --- internal/terraform/graph_builder_apply.go | 2 ++ internal/terraform/graph_builder_plan.go | 2 ++ internal/terraform/node_output.go | 5 ++++ internal/terraform/transform_output.go | 1 + .../terraform/transform_output_references.go | 30 +++++++++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 internal/terraform/transform_output_references.go diff --git a/internal/terraform/graph_builder_apply.go b/internal/terraform/graph_builder_apply.go index 83fe6b022e8a..c68e5a1ec9aa 100644 --- a/internal/terraform/graph_builder_apply.go +++ b/internal/terraform/graph_builder_apply.go @@ -188,6 +188,8 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &ReferenceTransformer{}, &AttachDependenciesTransformer{}, + &OutputReferencesTransformer{}, + // Nested data blocks should be loaded after every other resource has // done its thing. &checkStartTransformer{Config: b.Config, Operation: b.Operation}, diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index 3b6cfa847044..9f80750ae5c0 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -236,6 +236,8 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { &ReferenceTransformer{}, + &OutputReferencesTransformer{}, + &AttachDependenciesTransformer{}, // Make sure data sources are aware of any depends_on from the diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index b92f1a30d747..f504c7666606 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -46,6 +46,8 @@ type nodeExpandOutput struct { Overrides *mocking.Overrides Dependencies []addrs.ConfigResource + + Dependants []*addrs.Reference } var ( @@ -136,6 +138,7 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagn Planning: n.Planning, Override: n.getOverrideValue(absAddr.Module), Dependencies: n.Dependencies, + Dependants: n.Dependants, } } @@ -283,6 +286,8 @@ type NodeApplyableOutput struct { // Dependencies is the full set of resources that are referenced by this // output. Dependencies []addrs.ConfigResource + + Dependants []*addrs.Reference } var ( diff --git a/internal/terraform/transform_output.go b/internal/terraform/transform_output.go index 3bcfa5558f55..f0b6f19f12d4 100644 --- a/internal/terraform/transform_output.go +++ b/internal/terraform/transform_output.go @@ -67,6 +67,7 @@ func (t *OutputTransformer) transform(g *Graph, c *configs.Config) error { RefreshOnly: t.RefreshOnly, Planning: t.Planning, Overrides: t.Overrides, + Dependants: []*addrs.Reference{}, } log.Printf("[TRACE] OutputTransformer: adding %s as %T", o.Name, node) diff --git a/internal/terraform/transform_output_references.go b/internal/terraform/transform_output_references.go new file mode 100644 index 000000000000..91d0f18a64dd --- /dev/null +++ b/internal/terraform/transform_output_references.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +// OutputReferencesTransformer is a GraphTransformer that adds meta-information +// about references to output values in the configuration. This is used to +// determine when deprecated outputs are used. +type OutputReferencesTransformer struct{} + +func (t *OutputReferencesTransformer) Transform(g *Graph) error { + // Build a reference map so we can efficiently look up the references + vs := g.Vertices() + m := NewReferenceMap(vs) + + // Find the things that reference things and connect them + for _, v := range vs { + if dependant, ok := v.(GraphNodeReferencer); ok { + parents := m.References(v) + + for _, parent := range parents { + if output, ok := parent.(*nodeExpandOutput); ok { + output.Dependants = append(output.Dependants, dependant.References()...) + } + } + } + } + + return nil +} From 9d96887c77797562581b382ccafc2c18ce48f65a Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Fri, 15 Nov 2024 12:01:35 +0100 Subject: [PATCH 3/3] Add diagnostic for deprecated outputs --- internal/terraform/context_validate_test.go | 119 ++++++++++++++++++++ internal/terraform/node_output.go | 11 ++ 2 files changed, 130 insertions(+) diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 6452202437fb..d144c15cd6e6 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -3094,3 +3094,122 @@ module "child" { diags := ctx.Validate(m, &ValidateOpts{}) assertNoDiagnostics(t, diags) } + +func TestContext2Validate_deprecated_output(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} + +output "old-and-unused" { + deprecated = "This should not show up in the errors, we are not using it" + value = "old" +} + +output "new" { + value = "foo" +} +`, + "mod2/main.tf": ` +variable "input" { + type = string +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} + +resource "test_resource" "test" { + attr = module.mod.old +} + +resource "test_resource" "test2" { + attr = module.mod.new +} + +resource "test_resource" "test3" { + attr = module.mod.old +} + +output "test_output" { + value = module.mod.old +} + +module "mod2" { + source = "./mod2" + + input = module.mod.old +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + var expectedDiags tfdiags.Diagnostics + expectedDiags = expectedDiags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated output", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 12, Byte: 85}, + End: hcl.Pos{Line: 7, Column: 26, Byte: 99}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated output", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 15, Column: 12, Byte: 213}, + End: hcl.Pos{Line: 15, Column: 26, Byte: 227}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated output", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 19, Column: 10, Byte: 263}, + End: hcl.Pos{Line: 19, Column: 24, Byte: 277}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated output", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 25, Column: 10, Byte: 326}, + End: hcl.Pos{Line: 25, Column: 24, Byte: 340}, + }, + }, + ) + + assertDiagnosticsMatch(t, diags, expectedDiags) +} diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index f504c7666606..aa599900a49b 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -384,6 +384,17 @@ func (n *NodeApplyableOutput) References() []*addrs.Reference { // GraphNodeExecutable func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { + if op == walkValidate && n.Config.DeprecatedSet && len(n.Dependants) > 0 { + for _, d := range n.Dependants { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Usage of deprecated output", + Detail: n.Config.Deprecated, + Subject: d.SourceRange.ToHCL().Ptr(), + }) + } + } + state := ctx.State() if state == nil { return