diff --git a/internal/addrs/module_call.go b/internal/addrs/module_call.go index 95a8fb603da8..1d742a03cfc3 100644 --- a/internal/addrs/module_call.go +++ b/internal/addrs/module_call.go @@ -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. // @@ -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 { 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") + } +} diff --git a/internal/terraform/context.go b/internal/terraform/context.go index 425c4faf66ad..386e2c362fa8 100644 --- a/internal/terraform/context.go +++ b/internal/terraform/context.go @@ -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" @@ -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 diff --git a/internal/terraform/context_validate.go b/internal/terraform/context_validate.go index af484a579535..f81e83dfe2b8 100644 --- a/internal/terraform/context_validate.go +++ b/internal/terraform/context_validate.go @@ -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 diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 44083b467e93..e016f3c3b2ed 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -1858,7 +1858,7 @@ func TestContext2Validate_sensitiveProvisionerConfig(t *testing.T) { } } -func TestContext2Plan_validateMinMaxDynamicBlock(t *testing.T) { +func TestContext2Validate_validateMinMaxDynamicBlock(t *testing.T) { p := new(testing_provider.MockProvider) p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ @@ -2016,7 +2016,7 @@ resource "test_object" "t" { } } -func TestContext2Plan_lookupMismatchedObjectTypes(t *testing.T) { +func TestContext2Validate_lookupMismatchedObjectTypes(t *testing.T) { p := new(testing_provider.MockProvider) p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ @@ -3846,3 +3846,661 @@ func TestContext2Validate_noListValidated(t *testing.T) { }) } } + +func TestContext2Validate_deprecated_output_used_in_for_each(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = toset(["old"]) +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +resource "test_resource" "test" { + for_each = module.mod.old # WARNING + attr = "not-deprecated" +} +`, + }) + + 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{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 16, Byte: 88}, + End: hcl.Pos{Line: 6, Column: 30, Byte: 102}, + }, + })) +} + +func TestContext2Validate_deprecated_output_used_in_count(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = 42 +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +resource "test_resource" "test" { + count = module.mod.old # WARNING + attr = "not-deprecated" +} +`, + }) + + 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{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 16, Byte: 88}, + End: hcl.Pos{Line: 6, Column: 30, Byte: 102}, + }, + })) +} + +func TestContext2Validate_deprecated_output_used_in_resource_config(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 # WARNING +} +resource "test_resource" "test2" { + attr = module.mod.new # OK +} +resource "test_resource" "test3" { + attr = module.mod.old # WARNING +} +output "test_output" { + value = module.mod.old # WARNING +} +output "test_output_conditional" { + value = false ? module.mod.old : module.mod.new # WARNING +} + +`, + }) + + 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{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 12, Byte: 84}, + End: hcl.Pos{Line: 6, Column: 26, Byte: 98}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 12, Column: 12, Byte: 225}, + End: hcl.Pos{Line: 12, Column: 26, Byte: 239}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 15, Column: 10, Byte: 284}, + End: hcl.Pos{Line: 15, Column: 24, Byte: 298}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 18, Column: 18, Byte: 363}, + End: hcl.Pos{Line: 18, Column: 32, Byte: 377}, + }, + })) +} + +func TestContext2Validate_deprecated_output_only_triggers_once(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "nested/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +`, + "mod/main.tf": ` +module "nested" { + source = "../nested" +} +# This is an acceptable use of a deprecated value, so no warning +output "redeprecated" { + deprecated = "This should not be in use, dependency is deprecated" + value = module.nested.old +} +`, + + "mod2/main.tf": ` +module "nested" { + source = "../nested" +} +# This is an unacceptable use of a deprecated value, so warning +# but the value of this is not deprecated, we want the warning to exit where it is used +# not multiple times for the same value +output "undeprecated_use_of_deprecated_value" { + value = module.nested.old # WARNING +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +module "mod2" { + source = "./mod2" +} +resource "test_resource" "test" { + attr = module.mod.redeprecated # WARNING +} +resource "test_resource" "test2" { + attr = module.mod2.undeprecated_use_of_deprecated_value # OK - error was already thrown +} +output "test_output_deprecated_use" { + value = module.mod.redeprecated # WARNING +} +output "test_output_deprecated_use_with_deprecation" { + deprecated = "This is displayed in the UI, but does not produce an additional warning" + value = module.mod.redeprecated # OK +} +`, + }) + + 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{}) + + tfdiags.AssertDiagnosticsMatch( + t, + diags, + tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "This should not be in use, dependency is deprecated", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 9, Column: 11, Byte: 124}, + End: hcl.Pos{Line: 9, Column: 28, Byte: 147}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "This should not be in use, dependency is deprecated", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 15, Column: 10, Byte: 336}, + End: hcl.Pos{Line: 15, Column: 33, Byte: 359}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "mod2", "main.tf"), + Start: hcl.Pos{Line: 9, Column: 11, Byte: 294}, + End: hcl.Pos{Line: 9, Column: 28, Byte: 311}, + }, + }, + ), + ) +} + +func TestContext2Validate_deprecated_output_expansion(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/nested/main.tf": ` +output "old" { + deprecated = "mod/nested: Please stop using this" + value = "old" +} +`, + "mod/main.tf": ` +output "old" { + deprecated = "mod: Please stop using this" + value = "old" +} +module "modnested" { + source = "./nested" +} +output "new" { + deprecated = "mod: The dependency is deprecated, please stop using this" + value = module.modnested.old +} +`, + "mod2/main.tf": ` +output "old" { + deprecated = "mod2: Please stop using this" + value = "old" +} +output "new" { + value = "new" +} +`, + "mod3/main.tf": ` +output "old" { + deprecated = "mod2: Please stop using this" + value = "old" +} +output "new" { + value = "new" +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +module "mod2" { + count = 2 + source = "./mod2" +} +module "mod3" { + count = 2 + source = "./mod3" +} +resource "test_resource" "foo" { + attr = module.mod.old # WARNING +} +resource "test_resource" "bar" { + attr = module.mod2[0].old # WARNING +} +resource "test_resource" "baz" { + attr = module.mod.new # WARNING +} +output "test_output_no_warning" { + value = module.mod3[0].new # OK +} +`, + }) + + 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: "Deprecated value used", + Detail: "mod: Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 14, Column: 10, Byte: 177}, + End: hcl.Pos{Line: 14, Column: 24, Byte: 191}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "mod2: Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 17, Column: 10, Byte: 246}, + End: hcl.Pos{Line: 17, Column: 28, Byte: 264}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "mod: The dependency is deprecated, please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 20, Column: 10, Byte: 319}, + End: hcl.Pos{Line: 20, Column: 24, Byte: 333}, + }, + }, + ) + + tfdiags.AssertDiagnosticsMatch(t, diags, expectedDiags) +} + +func TestContext2Validate_deprecated_output_expansion_with_splat(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +`, + "main.tf": ` +module "mod" { + count = 2 + source = "./mod" +} +output "test_output2" { + value = module.mod[*].old # WARNING +} +`, + }) + + 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{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 10, Byte: 80}, + End: hcl.Pos{Line: 7, Column: 27, Byte: 97}, + }, + }, + )) +} + +func TestContext2Validate_deprecated_output_used_in_check_block(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" +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +check "deprecated_check" { + assert { + condition = !strcontains(module.mod.old, "hello-world") + error_message = "Neither condition nor error_message should contain ${module.mod.old} deprecated value" + } +} +`, + }) + + 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{}) + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 21, Byte: 97}, + End: hcl.Pos{Line: 7, Column: 64, Byte: 140}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 8, Column: 21, Byte: 161}, + End: hcl.Pos{Line: 8, Column: 108, Byte: 248}, + }, + }, + )) +} + +func TestContext2Validate_deprecated_output_in_lifecycle_conditions(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} +`, + "main.tf": ` +module "mod" { + source = "./mod" +} +locals { + a = "a" + b = "b" +} +resource "test_resource" "test" { + attr = "not-the-problem" + lifecycle { + precondition { + condition = module.mod.old == "old" + error_message = "This is okay." + } + + postcondition { + condition = local.a != local.b + error_message = "This should error with deprecated usage: ${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{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 16, Column: 29, Byte: 207}, + End: hcl.Pos{Line: 16, Column: 52, Byte: 230}, + }, + }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 22, Column: 29, Byte: 389}, + End: hcl.Pos{Line: 22, Column: 89, Byte: 449}, + }, + }, + )) +}