diff --git a/internal/configs/configschema/validate_traversal.go b/internal/configs/configschema/validate_traversal.go index 9178768a9164..06d9e010752a 100644 --- a/internal/configs/configschema/validate_traversal.go +++ b/internal/configs/configschema/validate_traversal.go @@ -77,27 +77,6 @@ func (b *Block) StaticValidateTraversal(traversal hcl.Traversal) tfdiags.Diagnos } if attrS, exists := b.Attributes[name]; exists { - // Check for Deprecated status of this attribute. - // We currently can't provide the user with any useful guidance because - // the deprecation string is not part of the schema, but we can at - // least warn them. - // - // This purposely does not attempt to recurse into nested attribute - // types. Because nested attribute values are often not accessed via a - // direct traversal to the leaf attributes, we cannot reliably detect - // if a nested, deprecated attribute value is actually used from the - // traversal alone. More precise detection of deprecated attributes - // would require adding metadata like marks to the cty value itself, to - // be caught during evaluation. - if attrS.Deprecated { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: `Deprecated attribute`, - Detail: fmt.Sprintf(`The attribute %q is deprecated. Refer to the provider documentation for details.`, name), - Subject: next.SourceRange().Ptr(), - }) - } - // For attribute validation we will just apply the rest of the // traversal to an unknown value of the attribute type and pass // through HCL's own errors, since we don't want to replicate all diff --git a/internal/configs/configschema/validate_traversal_test.go b/internal/configs/configschema/validate_traversal_test.go index 80107ab88497..efe721fb5654 100644 --- a/internal/configs/configschema/validate_traversal_test.go +++ b/internal/configs/configschema/validate_traversal_test.go @@ -224,10 +224,6 @@ func TestStaticValidateTraversal(t *testing.T) { `obj.nested_map["key"].optional`, ``, }, - { - `obj.deprecated`, - `Deprecated attribute: The attribute "deprecated" is deprecated. Refer to the provider documentation for details.`, - }, } for _, test := range tests { diff --git a/internal/deprecation/deprecation.go b/internal/deprecation/deprecation.go index b34209ce6e65..cce462d58018 100644 --- a/internal/deprecation/deprecation.go +++ b/internal/deprecation/deprecation.go @@ -43,21 +43,7 @@ func (d *Deprecations) Validate(value cty.Value, module addrs.Module, rng *hcl.R } notDeprecatedValue := marks.RemoveDeprecationMarks(value) - - // Check if we need to suppress deprecation warnings for this module call. - if d.IsModuleCallDeprecationSuppressed(module) { - return notDeprecatedValue, diags - } - - for _, depMark := range deprecationMarks { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Deprecated value used", - Detail: depMark.Message, - Subject: rng, - }) - } - + diags = diags.Append(d.diagnosticsForDeprecationMarks(deprecationMarks, module, rng)) return notDeprecatedValue, diags } @@ -86,6 +72,27 @@ func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tf return diags } +func (d *Deprecations) DiagnosticsForValueMarks(valueMarks cty.ValueMarks, module addrs.Module, rng *hcl.Range) tfdiags.Diagnostics { + return d.diagnosticsForDeprecationMarks(marks.FilterDeprecationMarks(valueMarks), module, rng) +} + +func (d *Deprecations) diagnosticsForDeprecationMarks(deprecationMarks []marks.DeprecationMark, module addrs.Module, rng *hcl.Range) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + // Check if we need to suppress deprecation warnings for this module call. + if !d.IsModuleCallDeprecationSuppressed(module) { + for _, depMark := range deprecationMarks { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: depMark.Message, + Subject: rng, + }) + } + } + + return diags +} + func (d *Deprecations) IsModuleCallDeprecationSuppressed(addr addrs.Module) bool { for _, mod := range d.suppressedModules { if mod.TargetContains(addr) { diff --git a/internal/deprecation/schema.go b/internal/deprecation/schema.go new file mode 100644 index 000000000000..6fd4d58ae2f9 --- /dev/null +++ b/internal/deprecation/schema.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package deprecation + +import ( + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +// MarkDeprecatedValues inspects the given cty.Value according to the given +// configschema.Block schema, and marks any deprecated attributes or blocks +// found within the value with deprecation marks. +// It works based on the given cty.Value's structure matching the given schema. +func MarkDeprecatedValues(val cty.Value, schema *configschema.Block) cty.Value { + if schema == nil { + return val + } + newVal := val + + // Check if the block is deprecated + if schema.Deprecated { + newVal = newVal.Mark(marks.NewDeprecation("deprecated resource block used")) + } + + if !newVal.IsKnown() { + return newVal + } + + // Even if the block itself is not deprecated, its attributes might be + // deprecated as well + if val.Type().IsObjectType() || val.Type().IsMapType() || val.Type().IsCollectionType() { + // We ignore the error, so errors are not allowed in the transform function + newVal, _ = cty.Transform(newVal, func(p cty.Path, v cty.Value) (cty.Value, error) { + + attr := schema.AttributeByPath(p) + if attr != nil && attr.Deprecated { + v = v.Mark(marks.NewDeprecation("deprecated resource attribute used")) + } + + block := schema.BlockByPath(p) + if block != nil && block.Deprecated { + v = v.Mark(marks.NewDeprecation("deprecated resource block used")) + } + + return v, nil + }) + } + + return newVal +} diff --git a/internal/deprecation/schema_test.go b/internal/deprecation/schema_test.go new file mode 100644 index 000000000000..fd850afc0b0a --- /dev/null +++ b/internal/deprecation/schema_test.go @@ -0,0 +1,1019 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package deprecation + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +func TestMarkDeprecatedValues_NilSchema(t *testing.T) { + val := cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }) + + result := MarkDeprecatedValues(val, nil) + + if !result.RawEquals(val) { + t.Errorf("expected value to be unchanged when schema is nil") + } +} + +func TestMarkDeprecatedValues_NoDeprecations(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + Deprecated: false, + }, + "bar": { + Type: cty.Number, + Optional: true, + Deprecated: false, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("hello"), + "bar": cty.NumberIntVal(42), + }) + + result := MarkDeprecatedValues(val, schema) + + if result.IsMarked() { + t.Errorf("expected value to not be marked when nothing is deprecated") + } + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + if len(pathMarks) > 0 { + t.Errorf("expected no marks, got %d marks", len(pathMarks)) + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_DeprecatedBlock(t *testing.T) { + schema := &configschema.Block{ + Deprecated: true, + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }) + + result := MarkDeprecatedValues(val, schema) + + if !result.IsMarked() { + t.Fatalf("expected result to be marked") + } + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path") + } + + // The root value itself should be marked as deprecated + foundRootDeprecation := false + for _, pvm := range pathMarks { + if len(pvm.Path) == 0 { + for mark := range pvm.Marks { + if _, ok := mark.(marks.DeprecationMark); ok { + foundRootDeprecation = true + break + } + } + } + } + + if !foundRootDeprecation { + t.Errorf("expected root value to be marked as deprecated") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_DeprecatedAttribute(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "deprecated_attr": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "normal_attr": { + Type: cty.String, + Optional: true, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "deprecated_attr": cty.StringVal("old"), + "normal_attr": cty.StringVal("new"), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) != 1 { + t.Fatalf("expected exactly 1 deprecated path, got %d", len(deprecatedPaths)) + } + + expectedPath := cty.GetAttrPath("deprecated_attr") + if !deprecatedPaths[0].Equals(expectedPath) { + t.Errorf("expected deprecated path to be %#v, got %#v", expectedPath, deprecatedPaths[0]) + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_MultipleDeprecatedAttributes(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "deprecated_one": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "deprecated_two": { + Type: cty.Number, + Optional: true, + Deprecated: true, + }, + "normal_attr": { + Type: cty.Bool, + Optional: true, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "deprecated_one": cty.StringVal("old1"), + "deprecated_two": cty.NumberIntVal(123), + "normal_attr": cty.BoolVal(true), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) != 2 { + t.Fatalf("expected exactly 2 deprecated paths, got %d", len(deprecatedPaths)) + } + + pathSet := make(map[string]bool) + for _, p := range deprecatedPaths { + if len(p) == 1 { + if getAttr, ok := p[0].(cty.GetAttrStep); ok { + pathSet[getAttr.Name] = true + } + } + } + + if !pathSet["deprecated_one"] || !pathSet["deprecated_two"] { + t.Errorf("expected both deprecated_one and deprecated_two to be marked as deprecated") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedBlock(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Optional: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "nested": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Deprecated: true, + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test"), + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("item1"), + }), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path for nested block") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedDeprecatedAttribute(t *testing.T) { + schema := &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "config": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "deprecated_field": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "normal_field": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "config": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "deprecated_field": cty.StringVal("old"), + "normal_field": cty.StringVal("new"), + }), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path") + } + + // Check that the deprecated field within the nested block is marked + foundDeprecatedField := false + for _, pvm := range pathMarks { + for i, step := range pvm.Path { + if getAttr, ok := step.(cty.GetAttrStep); ok && getAttr.Name == "deprecated_field" { + // Check if it's inside the config list + if i > 0 { + foundDeprecatedField = true + break + } + } + } + } + + if !foundDeprecatedField { + t.Errorf("expected nested deprecated_field to be marked") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NonObjectTypes(t *testing.T) { + tests := []struct { + name string + schema *configschema.Block + val cty.Value + }{ + { + name: "string value", + schema: &configschema.Block{ + Deprecated: false, + }, + val: cty.StringVal("test"), + }, + { + name: "number value", + schema: &configschema.Block{ + Deprecated: false, + }, + val: cty.NumberIntVal(42), + }, + { + name: "bool value", + schema: &configschema.Block{ + Deprecated: false, + }, + val: cty.BoolVal(true), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MarkDeprecatedValues(tt.val, tt.schema) + + // For non-object types, the function should handle gracefully + // and not crash + if result.IsNull() { + t.Errorf("result should not be null") + } + }) + } +} + +func TestMarkDeprecatedValues_DeprecatedBlockAndAttribute(t *testing.T) { + schema := &configschema.Block{ + Deprecated: true, + Attributes: map[string]*configschema.Attribute{ + "deprecated_attr": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "normal_attr": { + Type: cty.String, + Optional: true, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "deprecated_attr": cty.StringVal("old"), + "normal_attr": cty.StringVal("new"), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + // Should have both the block itself and the deprecated attribute marked + if len(deprecatedPaths) < 1 { + t.Fatalf("expected at least 1 deprecated path, got %d", len(deprecatedPaths)) + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_EmptyObject(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + } + + val := cty.EmptyObjectVal + + result := MarkDeprecatedValues(val, schema) + + // Should not crash on empty object + if result.IsNull() { + t.Errorf("result should not be null for empty object") + } +} + +func TestMarkDeprecatedValues_NullValue(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + } + + val := cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + })) + + result := MarkDeprecatedValues(val, schema) + + // Should handle null values gracefully + if !result.IsNull() { + t.Errorf("null input should remain null") + } +} + +func TestMarkDeprecatedValues_MapType(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "tags": { + Type: cty.Map(cty.String), + Optional: true, + Deprecated: false, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "tags": cty.MapVal(map[string]cty.Value{ + "env": cty.StringVal("prod"), + "team": cty.StringVal("platform"), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + // Should handle map types without crashing + unmarkedResult, _ := result.UnmarkDeepWithPaths() + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_ListType(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "items": { + Type: cty.List(cty.String), + Optional: true, + Deprecated: true, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "items": cty.ListVal([]cty.Value{ + cty.StringVal("one"), + cty.StringVal("two"), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected deprecated list attribute to be marked") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_NestingSingle(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + "config": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "deprecated_field": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "normal_field": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + "config": cty.ObjectVal(map[string]cty.Value{ + "deprecated_field": cty.StringVal("old"), + "normal_field": cty.StringVal("new"), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path") + } + + // Check that the deprecated field within the nested type is marked + foundDeprecatedField := false + for _, path := range deprecatedPaths { + if len(path) >= 2 { + if getAttr, ok := path[0].(cty.GetAttrStep); ok && getAttr.Name == "config" { + if getAttr2, ok := path[1].(cty.GetAttrStep); ok && getAttr2.Name == "deprecated_field" { + foundDeprecatedField = true + break + } + } + } + } + + if !foundDeprecatedField { + t.Errorf("expected config.deprecated_field to be marked as deprecated") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_NestingList(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + "disks": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "mount_point": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "size": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/mnt/data"), + "size": cty.StringVal("100GB"), + }), + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/mnt/backup"), + "size": cty.StringVal("200GB"), + }), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path") + } + + // Should have deprecated marks for mount_point in both list items + mountPointCount := 0 + for _, path := range deprecatedPaths { + for _, step := range path { + if getAttr, ok := step.(cty.GetAttrStep); ok && getAttr.Name == "mount_point" { + mountPointCount++ + break + } + } + } + + if mountPointCount != 2 { + t.Errorf("expected 2 deprecated mount_point fields, got %d", mountPointCount) + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_NestingSet(t *testing.T) { + // Note: The current implementation of AttributeByPath only handles GetAttrStep, + // not IndexStep, so nested attributes within set elements cannot be individually + // marked. This test verifies that the entire set attribute can still be marked + // if it is deprecated. + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "tags": { + Deprecated: true, // Mark the entire attribute as deprecated + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "key": { + Type: cty.String, + Optional: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "tags": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("env"), + "value": cty.StringVal("prod"), + }), + cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("team"), + "value": cty.StringVal("platform"), + }), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path") + } + + // The entire tags attribute should be marked as deprecated + foundDeprecatedTags := false + for _, path := range deprecatedPaths { + if len(path) == 1 { + if getAttr, ok := path[0].(cty.GetAttrStep); ok && getAttr.Name == "tags" { + foundDeprecatedTags = true + break + } + } + } + + if !foundDeprecatedTags { + t.Errorf("expected tags attribute to be marked as deprecated") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_NestingMap(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "metadata": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "description": { + Type: cty.String, + Optional: true, + }, + "deprecated_label": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "metadata": cty.MapVal(map[string]cty.Value{ + "primary": cty.ObjectVal(map[string]cty.Value{ + "description": cty.StringVal("Primary config"), + "deprecated_label": cty.StringVal("old_label"), + }), + "secondary": cty.ObjectVal(map[string]cty.Value{ + "description": cty.StringVal("Secondary config"), + "deprecated_label": cty.StringVal("old_label_2"), + }), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) == 0 { + t.Fatalf("expected at least one deprecated path") + } + + // Check that deprecated_label fields are marked + deprecatedLabelCount := 0 + for _, path := range deprecatedPaths { + for _, step := range path { + if getAttr, ok := step.(cty.GetAttrStep); ok && getAttr.Name == "deprecated_label" { + deprecatedLabelCount++ + break + } + } + } + + if deprecatedLabelCount != 2 { + t.Errorf("expected 2 deprecated_label fields, got %d", deprecatedLabelCount) + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_DeprecatedNestedAttribute(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "deprecated_config": { + Deprecated: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "field": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "deprecated_config": cty.ObjectVal(map[string]cty.Value{ + "field": cty.StringVal("value"), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) != 1 { + t.Fatalf("expected at least one deprecated path") + } + + // The entire deprecated_config attribute should be marked + foundDeprecatedConfig := false + for _, path := range deprecatedPaths { + if len(path) == 1 { + if getAttr, ok := path[0].(cty.GetAttrStep); ok && getAttr.Name == "deprecated_config" { + foundDeprecatedConfig = true + break + } + } + } + + if !foundDeprecatedConfig { + t.Errorf("expected deprecated_config to be marked as deprecated") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_MultipleDeprecatedFields(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "connection": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "deprecated_host": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + "deprecated_port": { + Type: cty.Number, + Optional: true, + Deprecated: true, + }, + "username": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "connection": cty.ObjectVal(map[string]cty.Value{ + "deprecated_host": cty.StringVal("example.com"), + "deprecated_port": cty.NumberIntVal(8080), + "username": cty.StringVal("admin"), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) != 2 { + t.Fatalf("expected exactly 2 deprecated paths, got %d", len(deprecatedPaths)) + } + + // Check that both deprecated_host and deprecated_port are marked + pathSet := make(map[string]bool) + for _, path := range deprecatedPaths { + if len(path) == 2 { + if getAttr, ok := path[1].(cty.GetAttrStep); ok { + pathSet[getAttr.Name] = true + } + } + } + + if !pathSet["deprecated_host"] || !pathSet["deprecated_port"] { + t.Errorf("expected both deprecated_host and deprecated_port to be marked as deprecated") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_EmptyList(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "items": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "deprecated_field": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "items": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "deprecated_field": cty.String, + })), + }) + + result := MarkDeprecatedValues(val, schema) + + // Should handle empty lists without crashing + if result.IsNull() { + t.Errorf("result should not be null for empty list") + } + + unmarkedResult, _ := result.UnmarkDeepWithPaths() + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_NullNestedValue(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "config": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "deprecated_field": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "config": cty.NullVal(cty.Object(map[string]cty.Type{ + "deprecated_field": cty.String, + })), + }) + + result := MarkDeprecatedValues(val, schema) + + // Should handle null nested values gracefully + if result.IsNull() { + t.Errorf("result should not be null") + } + + unmarkedResult, _ := result.UnmarkDeepWithPaths() + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} + +func TestMarkDeprecatedValues_NestedType_MixedWithBlockTypes(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_attr": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "deprecated_field": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "nested_block": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "deprecated_block_attr": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + } + + val := cty.ObjectVal(map[string]cty.Value{ + "nested_attr": cty.ObjectVal(map[string]cty.Value{ + "deprecated_field": cty.StringVal("attr_value"), + }), + "nested_block": cty.ObjectVal(map[string]cty.Value{ + "deprecated_block_attr": cty.StringVal("block_value"), + }), + }) + + result := MarkDeprecatedValues(val, schema) + + unmarkedResult, pathMarks := result.UnmarkDeepWithPaths() + deprecatedPaths, _ := marks.PathsWithMark(pathMarks, marks.Deprecation) + + if len(deprecatedPaths) != 2 { + t.Fatalf("expected exactly 2 deprecated paths (one from NestedType, one from BlockType), got %d", len(deprecatedPaths)) + } + + // Check that both deprecated fields are marked + pathSet := make(map[string]bool) + for _, path := range deprecatedPaths { + if len(path) >= 2 { + if getAttr, ok := path[1].(cty.GetAttrStep); ok { + pathSet[getAttr.Name] = true + } + } + } + + if !pathSet["deprecated_field"] || !pathSet["deprecated_block_attr"] { + t.Errorf("expected both deprecated_field and deprecated_block_attr to be marked") + } + + if !unmarkedResult.RawEquals(val) { + t.Errorf("expected unmarked value to equal original value") + } +} diff --git a/internal/lang/checks.go b/internal/lang/checks.go index 9ad8d91e04d2..9f4d5c95c0dd 100644 --- a/internal/lang/checks.go +++ b/internal/lang/checks.go @@ -22,13 +22,13 @@ import ( // It will either return a non-empty message string or it'll return diagnostics // with either errors or warnings that explain why the given expression isn't // acceptable. -func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAddr *addrs.CheckRule) (string, tfdiags.Diagnostics) { +func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAddr *addrs.CheckRule) (string, cty.ValueMarks, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics val, hclDiags := expr.Value(hclCtx) diags = diags.Append(hclDiags) if hclDiags.HasErrors() { - return "", diags + return "", cty.NewValueMarks(), diags } val, err := convert.Convert(val, cty.String) @@ -41,10 +41,10 @@ func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAdd Expression: expr, EvalContext: hclCtx, }) - return "", diags + return "", cty.NewValueMarks(), diags } if !val.IsKnown() { - return "", diags + return "", cty.NewValueMarks(), diags } if val.IsNull() { diags = diags.Append(&hcl.Diagnostic{ @@ -55,7 +55,7 @@ func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAdd Expression: expr, EvalContext: hclCtx, }) - return "", diags + return "", cty.NewValueMarks(), diags } val, valMarks := val.Unmark() @@ -70,7 +70,7 @@ You can correct this by removing references to sensitive values, or by carefully Expression: expr, EvalContext: hclCtx, }) - return "", diags + return "", valMarks, diags } if _, ephemeral := valMarks[marks.Ephemeral]; ephemeral { @@ -90,11 +90,11 @@ You can correct this by removing references to ephemeral values, or by using the Subject: expr.Range().Ptr(), Extra: extra, }) - return "", diags + return "", valMarks, diags } // NOTE: We've discarded any other marks the string might have been carrying, // aside from the sensitive mark. - return strings.TrimSpace(val.AsString()), diags + return strings.TrimSpace(val.AsString()), valMarks, diags } diff --git a/internal/moduletest/graph/eval_context.go b/internal/moduletest/graph/eval_context.go index dc356639a569..4d8059520c68 100644 --- a/internal/moduletest/graph/eval_context.go +++ b/internal/moduletest/graph/eval_context.go @@ -319,7 +319,7 @@ func (ec *EvalContext) EvaluateRun(run *configs.TestRun, module *configs.Module, continue } - errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil) + errorMessage, _, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil) ruleDiags = ruleDiags.Append(moreDiags) runVal, hclDiags := rule.Condition.Value(hclCtx) diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 44083b467e93..3d93aad90161 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" @@ -2459,37 +2460,446 @@ resource "aws_instance" "test" { } func TestContext2Validate_deprecatedAttr(t *testing.T) { - p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ - ResourceTypes: map[string]*configschema.Block{ - "aws_instance": { - Attributes: map[string]*configschema.Attribute{ - "foo": {Type: cty.String, Optional: true, Deprecated: true}, + for name, tc := range map[string]struct { + attributeSchema map[string]*configschema.Attribute + blockSchema map[string]*configschema.NestedBlock + module map[string]string + expectedValidationDiags func(*configs.Config) tfdiags.Diagnostics + expectedPlanDiags func(*configs.Config) tfdiags.Diagnostics + }{ + "in locals": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + } + locals { + deprecated = aws_instance.test.foo + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 5, Column: 28, Byte: 108}, + End: hcl.Pos{Line: 5, Column: 49, Byte: 129}, + }, + }) + }, + }, + + "in count": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.Number, Required: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + foo = 2 + } + resource "aws_instance" "test2" { + count = aws_instance.test.foo + foo = 1 + } + `, + }, + expectedPlanDiags: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 23, Byte: 152}, + End: hcl.Pos{Line: 6, Column: 44, Byte: 173}, + }, + }) + }, + }, + + "in for_each": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.Set(cty.String), Required: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + foo = ["a", "b"] + } + resource "aws_instance" "test2" { + for_each = aws_instance.test.foo + foo = ["x"] + } + `, + }, + expectedPlanDiags: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 26, Byte: 164}, + End: hcl.Pos{Line: 6, Column: 47, Byte: 185}, + }, + }) + }, + }, + + "in output": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + } + output "deprecated_output" { + value = aws_instance.test.foo + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 5, Column: 23, Byte: 123}, + End: hcl.Pos{Line: 5, Column: 44, Byte: 144}, + }, + }) + }, + }, + + "in resource attribute": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Deprecated: true}, + "bar": {Type: cty.String, Optional: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + } + resource "aws_instance" "test2" { + bar = aws_instance.test.foo + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 5, Column: 21, Byte: 126}, + End: hcl.Pos{Line: 5, Column: 42, Byte: 147}, + }, + }) + }, + }, + + "in precondition": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Required: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + foo = "bar" + } + resource "aws_instance" "test2" { + foo = "baz" + lifecycle { + precondition { + condition = aws_instance.test.foo != "" + error_message = "foo must not be empty" + } + } + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 9, Column: 31, Byte: 245}, + End: hcl.Pos{Line: 9, Column: 58, Byte: 272}, + }, + }) + }, + }, + + "in postcondition condition": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Required: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + foo = "bar" + lifecycle { + postcondition { + condition = self.foo != "" + error_message = "foo must not be empty" + } + } + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 31, Byte: 160}, + End: hcl.Pos{Line: 6, Column: 45, Byte: 174}, + }, + }) + }, + }, + + "in dynamic block": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.Set(cty.String), Computed: true, Deprecated: true}, + }, + blockSchema: map[string]*configschema.NestedBlock{ + "bar": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "baz": { + Type: cty.String, + Required: false, + Optional: true, + }, + }, + }, }, }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + foo = ["a", "b"] + } + resource "aws_instance" "test2" { + dynamic "bar" { + for_each = aws_instance.test.foo + content { + baz = bar.value + } + } + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 5, Column: 45, Byte: 135}, + End: hcl.Pos{Line: 5, Column: 45, Byte: 135}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 5, Column: 45, Byte: 135}, + End: hcl.Pos{Line: 5, Column: 45, Byte: 135}, + }, + }) + }, }, - }) - m := testModuleInline(t, map[string]string{ - "main.tf": ` -resource "aws_instance" "test" { -} -locals { - deprecated = aws_instance.test.foo -} - `, - }) + "in check assertion": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Required: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + foo = "bar" + } + check "test_check" { + assert { + condition = aws_instance.test.foo != "" + error_message = "foo must not be empty" + } + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 29, Byte: 170}, + End: hcl.Pos{Line: 7, Column: 56, Byte: 197}, + }, + }) + }, + }, - ctx := testContext2(t, &ContextOpts{ - Providers: map[addrs.Provider]providers.Factory{ - addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + "in module input": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + } + module "child" { + source = "./child" + input = aws_instance.test.foo + } + `, + "child/main.tf": ` + variable "input" { + type = string + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 23, Byte: 144}, + End: hcl.Pos{Line: 6, Column: 44, Byte: 165}, + }, + }) + }, }, - }) - diags := ctx.Validate(m, nil) - warn := diags.ErrWithWarnings().Error() - if !strings.Contains(warn, `The attribute "foo" is deprecated`) { - t.Fatalf("expected deprecated warning, got: %q\n", warn) + "in connection block": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + } + resource "aws_instance" "test2" { + provisioner "shell" { + test_string = aws_instance.test.foo + connection { + type = "ssh" + host = aws_instance.test.foo + } + } + } + `, + }, + expectedValidationDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `The attribute "foo" is deprecated`, + Detail: `The attribute "foo" is deprecated and will be removed in a future version.`, + }) + }, + }, + + "in action config": { + attributeSchema: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Deprecated: true}, + }, + module: map[string]string{ + "main.tf": ` + resource "aws_instance" "test" { + } + action "aws_register" "example" { + config { + host = aws_instance.test.foo + } + } + `, + }, + expectedPlanDiags: func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: `Deprecated value used`, + Detail: `deprecated resource attribute used`, + Subject: &hcl.Range{ + Filename: filepath.Join(c.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 24, Byte: 152}, + End: hcl.Pos{Line: 6, Column: 45, Byte: 173}, + }, + }) + }, + }, + } { + t.Run(name, func(t *testing.T) { + // Default values + if tc.expectedValidationDiags == nil { + tc.expectedValidationDiags = func(c *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{} + } + } + // By default we want the same validations in plan as in validate + if tc.expectedPlanDiags == nil { + tc.expectedPlanDiags = tc.expectedValidationDiags + } + pr := simpleMockProvisioner() + p := testProvider("aws") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: tc.attributeSchema, + BlockTypes: tc.blockSchema, + }, + }, + Actions: map[string]*providers.ActionSchema{ + "aws_register": { + ConfigSchema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "host": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + }) + m := testModuleInline(t, tc.module) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + Provisioners: map[string]provisioners.Factory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + t.Run("validate", func(t *testing.T) { + validateDiags := ctx.Validate(m, nil) + tfdiags.AssertDiagnosticsMatch(t, validateDiags, tc.expectedValidationDiags(m)) + }) + + t.Run("plan", func(t *testing.T) { + _, planDiags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, InputValues{})) + tfdiags.AssertDiagnosticsMatch(t, planDiags, tc.expectedPlanDiags(m)) + }) + }) } } diff --git a/internal/terraform/eval_conditions.go b/internal/terraform/eval_conditions.go index 6a221339e439..2f5af6e84378 100644 --- a/internal/terraform/eval_conditions.go +++ b/internal/terraform/eval_conditions.go @@ -103,8 +103,9 @@ func validateCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalCo hclCtx, moreDiags := scope.EvalContext(refs) diags = diags.Append(moreDiags) - errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, &addr) + errorMessage, errorMessageValMarks, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, &addr) diags = diags.Append(moreDiags) + diags = diags.Append(ctx.Deprecations().DiagnosticsForValueMarks(errorMessageValMarks, ctx.Path().Module(), rule.ErrorMessage.Range().Ptr())) return errorMessage, hclCtx, diags } @@ -120,6 +121,10 @@ func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContex resultVal, hclDiags := rule.Condition.Value(hclCtx) diags = diags.Append(hclDiags) + var deprecationDiags tfdiags.Diagnostics + resultVal, deprecationDiags = ctx.Deprecations().Validate(resultVal, ctx.Path().Module(), rule.Condition.Range().Ptr()) + diags = diags.Append(deprecationDiags) + if diags.HasErrors() { log.Printf("[TRACE] evalCheckRule: %s: %s", addr.Type, diags.Err().Error()) return checkResult{Status: checks.StatusError}, diags diff --git a/internal/terraform/eval_count.go b/internal/terraform/eval_count.go index 0c2827a2f06f..36c3bf8b6b61 100644 --- a/internal/terraform/eval_count.go +++ b/internal/terraform/eval_count.go @@ -87,6 +87,9 @@ func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Val return nullCount, diags } + countVal, deprecationDiags := ctx.Deprecations().Validate(countVal, ctx.Path().Module(), expr.Range().Ptr()) + diags = diags.Append(deprecationDiags) + if marks.Has(countVal, marks.Ephemeral) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, diff --git a/internal/terraform/eval_count_test.go b/internal/terraform/eval_count_test.go index 307a507771e0..5985dd993955 100644 --- a/internal/terraform/eval_count_test.go +++ b/internal/terraform/eval_count_test.go @@ -11,6 +11,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcltest" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) @@ -31,7 +32,11 @@ func TestEvaluateCountExpression(t *testing.T) { } for name, test := range tests { t.Run(name, func(t *testing.T) { - ctx := &MockEvalContext{} + ctx := &MockEvalContext{ + Scope: evalContextModuleInstance{ + Addr: addrs.RootModuleInstance, + }, + } ctx.installSimpleEval() countVal, diags := evaluateCountExpression(test.Expr, ctx, false) @@ -51,7 +56,11 @@ func TestEvaluateCountExpression(t *testing.T) { func TestEvaluateCountExpression_ephemeral(t *testing.T) { expr := hcltest.MockExprLiteral(cty.NumberIntVal(8).Mark(marks.Ephemeral)) - ctx := &MockEvalContext{} + ctx := &MockEvalContext{ + Scope: evalContextModuleInstance{ + Addr: addrs.RootModuleInstance, + }, + } ctx.installSimpleEval() _, diags := evaluateCountExpression(expr, ctx, false) if !diags.HasErrors() { diff --git a/internal/terraform/eval_for_each.go b/internal/terraform/eval_for_each.go index 0abac36dda13..b1e55a889a3c 100644 --- a/internal/terraform/eval_for_each.go +++ b/internal/terraform/eval_for_each.go @@ -86,6 +86,7 @@ func (ev *forEachEvaluator) ResourceValue() (map[string]cty.Value, bool, tfdiags // validate the for_each value for use in resource expansion diags = diags.Append(ev.validateResourceOrActionForEach(forEachVal, "resource")) + forEachVal = marks.RemoveDeprecationMarks(forEachVal) if diags.HasErrors() { return res, false, diags } @@ -328,6 +329,9 @@ func (ev *forEachEvaluator) validateResourceOrActionForEach(forEachVal cty.Value } diags = diags.Append(ev.ensureNotEphemeral(forEachVal)) + var deprecationDiags tfdiags.Diagnostics + forEachVal, deprecationDiags = ev.ctx.Deprecations().Validate(forEachVal, ev.ctx.Path().Module(), ev.expr.Range().Ptr()) + diags = diags.Append(deprecationDiags) if diags.HasErrors() { return diags diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index 343be4034a11..d9fbaa80eca8 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/deprecation" "github.com/hashicorp/terraform/internal/didyoumean" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" @@ -627,9 +628,13 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc // resource has (using d.Evaluator.Instances.ResourceInstanceKeys) and // then retrieving the value for each instance to assemble into the // result, using some per-resource-mode logic maintained elsewhere. - return d.getEphemeralResource(addr, rng) + val, ephemeralDiags := d.getEphemeralResource(addr, rng) + diags = diags.Append(ephemeralDiags) + return deprecation.MarkDeprecatedValues(val, schema.Body), diags case addrs.ListResourceMode: - return d.getListResource(config, rng) + val, listDiags := d.getListResource(config, rng) + diags = diags.Append(listDiags) + return deprecation.MarkDeprecatedValues(val, schema.Body), diags default: // continue with the rest of the function } @@ -804,7 +809,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc continue } - vals[int(intKey)] = instance + vals[int(intKey)] = deprecation.MarkDeprecatedValues(instance, schema.Body) } // Insert unknown values where there are any missing instances @@ -826,7 +831,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc // old key that is being dropped and not used for evaluation continue } - vals[string(strKey)] = instance + vals[string(strKey)] = deprecation.MarkDeprecatedValues(instance, schema.Body) } if len(vals) > 0 { @@ -846,7 +851,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc val = cty.UnknownVal(ty) } - ret = val + ret = deprecation.MarkDeprecatedValues(val, schema.Body) } return ret, diags diff --git a/internal/terraform/node_action_instance.go b/internal/terraform/node_action_instance.go index cfcc89338d7b..4687120d9d29 100644 --- a/internal/terraform/node_action_instance.go +++ b/internal/terraform/node_action_instance.go @@ -71,7 +71,10 @@ func (n *NodeActionDeclarationInstance) Execute(ctx EvalContext, _ walkOperation valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, n.Schema.ConfigSchema) diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.Addr.String())) - if valDiags.HasErrors() { + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()) + diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String())) + + if diags.HasErrors() { return diags } } diff --git a/internal/terraform/node_action_partialexp.go b/internal/terraform/node_action_partialexp.go index f1abe3e20f4f..69478d687339 100644 --- a/internal/terraform/node_action_partialexp.go +++ b/internal/terraform/node_action_partialexp.go @@ -68,6 +68,12 @@ func (n *NodeActionDeclarationPartialExpanded) Execute(ctx EvalContext, op walkO if diags.HasErrors() { return diags } + + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.ActionAddr().Module) + diags = diags.Append(deprecationDiags) + if diags.HasErrors() { + return diags + } } ctx.Actions().AddPartialExpandedAction(n.addr, configVal, n.resolvedProvider) return nil diff --git a/internal/terraform/node_action_validate.go b/internal/terraform/node_action_validate.go index f80e2e204b6c..5a85f6adde79 100644 --- a/internal/terraform/node_action_validate.go +++ b/internal/terraform/node_action_validate.go @@ -105,6 +105,12 @@ func (n *NodeValidatableAction) Execute(ctx EvalContext, _ walkOperation) tfdiag valDiags = validateResourceForbiddenEphemeralValues(ctx, configVal, schema.ConfigSchema) diags = diags.Append(valDiags.InConfigBody(config, n.Addr.String())) + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Addr.Module) + diags = diags.Append(deprecationDiags.InConfigBody(config, n.Addr.String())) + if diags.HasErrors() { + return diags + } + // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() log.Printf("[TRACE] Validating config for %q", n.Addr) diff --git a/internal/terraform/node_local.go b/internal/terraform/node_local.go index dd25a98ab62c..9de11d116e2f 100644 --- a/internal/terraform/node_local.go +++ b/internal/terraform/node_local.go @@ -234,5 +234,10 @@ func evaluateLocalValue(config *configs.Local, localAddr addrs.LocalValue, addrS if val == cty.NilVal { val = cty.DynamicVal } + + var deprecationDiags tfdiags.Diagnostics + val, deprecationDiags = ctx.Deprecations().Validate(val, ctx.Path().Module(), expr.Range().Ptr()) + diags = diags.Append(deprecationDiags) + return val, diags } diff --git a/internal/terraform/node_module_variable.go b/internal/terraform/node_module_variable.go index 005edc4ebcc7..241a19478040 100644 --- a/internal/terraform/node_module_variable.go +++ b/internal/terraform/node_module_variable.go @@ -306,6 +306,10 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo finalVal, moreDiags := PrepareFinalInputVariableValue(n.Addr, rawVal, n.Config) diags = diags.Append(moreDiags) + var deprecationDiags tfdiags.Diagnostics + finalVal, deprecationDiags = ctx.Deprecations().Validate(finalVal, n.ModulePath(), errSourceRange.ToHCL().Ptr()) + diags = diags.Append(deprecationDiags) + return finalVal, diags.ErrWithWarnings() } diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index 3e505a28fb79..d035e6919cbb 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -451,6 +451,10 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil) diags = diags.Append(evalDiags) + var deprecationDiags tfdiags.Diagnostics + val, deprecationDiags = ctx.Deprecations().Validate(val, n.Addr.Module.Module(), n.Config.Expr.Range().Ptr()) + diags = diags.Append(deprecationDiags) + // We'll handle errors below, after we have loaded the module. // Outputs don't have a separate mode for validation, so validate // depends_on expressions here too @@ -586,6 +590,9 @@ func (n *nodeOutputInPartialModule) Execute(ctx EvalContext, op walkOperation) t if val == cty.NilVal { val = cty.DynamicVal } + var deprecationDiags tfdiags.Diagnostics + val, deprecationDiags = ctx.Deprecations().Validate(val, n.Addr.Module.Module(), n.Config.Expr.Range().Ptr()) + diags = diags.Append(deprecationDiags) // We'll also check that the depends_on argument is valid, since that's // a static concern anyway and so cannot vary between instances of the diff --git a/internal/terraform/node_provider.go b/internal/terraform/node_provider.go index e691451c4660..0b05761209de 100644 --- a/internal/terraform/node_provider.go +++ b/internal/terraform/node_provider.go @@ -78,10 +78,16 @@ func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, provider provi } configVal, _, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) - if evalDiags.HasErrors() { - return diags.Append(evalDiags) - } diags = diags.Append(evalDiags) + if diags.HasErrors() { + return diags + } + + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Addr.Module) + diags = diags.Append(deprecationDiags.InConfigBody(configBody, n.Addr.String())) + if diags.HasErrors() { + return diags + } // If our config value contains any marked values, ensure those are // stripped out before sending this to the provider diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 37321761bc14..914a9cb2f152 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -866,6 +866,9 @@ func (n *NodeAbstractResourceInstance) plan( diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, origConfigVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(origConfigVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ) if diags.HasErrors() { return nil, nil, deferred, keyData, diags } @@ -1771,6 +1774,9 @@ func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value var configDiags tfdiags.Diagnostics metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, providerSchema.ProviderMeta.Body, nil, EvalDataForNoInstanceKey) diags = diags.Append(configDiags) + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(metaConfigVal, ctx.Path().Module()).InConfigBody(m.Config, n.Addr.String()), + ) } } } @@ -1847,6 +1853,9 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(configVal, ctx.Path().Module()).InConfigBody(n.Config.Config, n.Addr.String()), + ) if diags.HasErrors() { return nil, nil, deferred, keyData, diags } @@ -2184,6 +2193,13 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned return nil, keyData, diags } + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ) + if diags.HasErrors() { + return nil, keyData, diags + } + newVal, readDeferred, readDiags := n.readDataSource(ctx, configVal) if check, nested := n.nestedInCheckBlock(); nested { addr := check.Addr().Absolute(n.Addr.Module) @@ -2389,6 +2405,8 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state if diags.HasErrors() { return diags } + + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(connInfo, n.ModulePath()).InConfigBody(connBody, n.Addr.String())) } { @@ -2496,6 +2514,8 @@ func (n *NodeAbstractResourceInstance) evalProvisionerConfig(ctx EvalContext, bo config, _, configDiags := ctx.EvaluateBlock(body, schema, n.ResourceInstanceAddr().Resource, keyData) diags = diags.Append(configDiags) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(config, n.ModulePath()).InConfigBody(body, n.Addr.String())) + return config, diags } @@ -2512,6 +2532,7 @@ func (n *NodeAbstractResourceInstance) evalDestroyProvisionerConfig(ctx EvalCont evalScope := ctx.EvaluationScope(n.ResourceInstanceAddr().Resource, nil, keyData) config, evalDiags := evalScope.EvalSelfBlock(body, self, schema, keyData) diags = diags.Append(evalDiags) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(config, n.ModulePath()).InConfigBody(body, n.Addr.String())) return config, diags } @@ -2561,6 +2582,13 @@ func (n *NodeAbstractResourceInstance) apply( if configDiags.HasErrors() { return state, diags } + + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(applyConfig.Config, n.Addr.String()), + ) + if diags.HasErrors() { + return state, diags + } } if !configVal.IsWhollyKnown() { diff --git a/internal/terraform/node_resource_ephemeral.go b/internal/terraform/node_resource_ephemeral.go index 79103269788d..0e6afbc41a2b 100644 --- a/internal/terraform/node_resource_ephemeral.go +++ b/internal/terraform/node_resource_ephemeral.go @@ -76,6 +76,12 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid if diags.HasErrors() { return nil, diags } + + diags = diags.Append(validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(config.Config, inp.addr.String())) + if diags.HasErrors() { + return nil, diags + } + unmarkedConfigVal, configMarks := configVal.UnmarkDeepWithPaths() if !unmarkedConfigVal.IsWhollyKnown() { diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 74f7140a37c9..0c4aa84d3655 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -638,6 +638,12 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. diags = diags.Append(configDiags) return nil, deferred, diags } + + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, absAddr.String())) + if diags.HasErrors() { + return nil, deferred, diags + } + configVal, _ = configVal.UnmarkDeep() // Let's pretend we're reading the value as a data source so we diff --git a/internal/terraform/node_resource_plan_instance_query.go b/internal/terraform/node_resource_plan_instance_query.go index 6e099b14f7a8..2bc68455ff16 100644 --- a/internal/terraform/node_resource_plan_instance_query.go +++ b/internal/terraform/node_resource_plan_instance_query.go @@ -49,6 +49,10 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di if diags.HasErrors() { return diags } + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(blockVal, n.ModulePath()).InConfigBody(config.Config, n.Addr.String())) + if diags.HasErrors() { + return diags + } // Unmark before sending to provider unmarkedBlockVal, _ := blockVal.UnmarkDeepWithPaths() @@ -63,6 +67,9 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di if limitDiags.HasErrors() { return diags } + var limitDeprecationDiags tfdiags.Diagnostics + limitCty, limitDeprecationDiags = ctx.Deprecations().Validate(limitCty, ctx.Path().Module(), config.List.Limit.Range().Ptr()) + diags = diags.Append(limitDeprecationDiags) includeRscCty, includeRsc, includeDiags := newIncludeRscEvaluator(false).EvaluateExpr(ctx, config.List.IncludeResource) diags = diags.Append(includeDiags) @@ -70,6 +77,10 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di return diags } + var includeDeprecationDiags tfdiags.Diagnostics + includeRscCty, includeDeprecationDiags = ctx.Deprecations().Validate(includeRscCty, ctx.Path().Module(), config.List.IncludeResource.Range().Ptr()) + diags = diags.Append(includeDeprecationDiags) + rId := HookResourceIdentity{ Addr: addr, ProviderAddr: n.ResolvedProvider.Provider, diff --git a/internal/terraform/node_resource_plan_partialexp.go b/internal/terraform/node_resource_plan_partialexp.go index d2675b0449fe..048d15923a2c 100644 --- a/internal/terraform/node_resource_plan_partialexp.go +++ b/internal/terraform/node_resource_plan_partialexp.go @@ -200,6 +200,11 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo return &change, diags } + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ResourceAddr().Module).InConfigBody(n.config.Config, n.addr.String())) + if diags.HasErrors() { + return &change, diags + } + unmarkedConfigVal, _ := configVal.UnmarkDeep() log.Printf("[TRACE] Validating partially expanded config for %q", n.addr) validateResp := provider.ValidateResourceConfig( @@ -354,6 +359,11 @@ func (n *nodePlannablePartialExpandedResource) dataResourceExecute(ctx EvalConte return &change, diags } + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ResourceAddr().Module).InConfigBody(n.config.Config, n.addr.String())) + if diags.HasErrors() { + return &change, diags + } + // Note: We're deliberately not doing anything special for nested-in-a-check // data sources. (*NodeAbstractResourceInstance).planDataSource has some // special handling for these, but it's founded on the assumption that we're diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index dc12110da8ad..266de28c7b42 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -141,7 +141,9 @@ func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *config func (n *NodeValidatableResource) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block) (cty.Value, hcl.Body, tfdiags.Diagnostics) { keyData, selfAddr := n.stubRepetitionData(n.Config.Count != nil, n.Config.ForEach != nil) - return ctx.EvaluateBlock(body, schema, selfAddr, keyData) + val, hclBody, diags := ctx.EvaluateBlock(body, schema, selfAddr, keyData) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(val, n.Addr.Module).InConfigBody(body, n.Addr.String())) + return val, hclBody, diags } // connectionBlockSupersetSchema is a schema representing the superset of all @@ -355,6 +357,9 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ) if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks for _, traversal := range n.Config.Managed.IgnoreChanges { @@ -434,6 +439,9 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ) // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() @@ -461,6 +469,9 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag if valDiags.HasErrors() { return diags } + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ) // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() req := providers.ValidateEphemeralResourceConfigRequest{ @@ -488,17 +499,28 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } + diags = diags.Append( + ctx.Deprecations().ValidateAsConfig(blockVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ) + limit, _, limitDiags := newLimitEvaluator(true).EvaluateExpr(ctx, n.Config.List.Limit) diags = diags.Append(limitDiags) if limitDiags.HasErrors() { return diags } + var limitDeprecationDiags tfdiags.Diagnostics + limit, limitDeprecationDiags = ctx.Deprecations().Validate(limit, n.ModulePath(), n.Config.List.Limit.Range().Ptr()) + diags = diags.Append(limitDeprecationDiags) + includeResource, _, includeDiags := newIncludeRscEvaluator(true).EvaluateExpr(ctx, n.Config.List.IncludeResource) diags = diags.Append(includeDiags) if includeDiags.HasErrors() { return diags } + var includeDeprecationDiags tfdiags.Diagnostics + includeResource, includeDeprecationDiags = ctx.Deprecations().Validate(includeResource, n.ModulePath(), n.Config.List.IncludeResource.Range().Ptr()) + diags = diags.Append(includeDeprecationDiags) // Use unmarked value for validate request unmarkedBlockVal, _ := blockVal.UnmarkDeep()