diff --git a/internal/configs/experiments.go b/internal/configs/experiments.go index 163e75e6d1..d3fd6a4e59 100644 --- a/internal/configs/experiments.go +++ b/internal/configs/experiments.go @@ -208,5 +208,28 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics { } */ + if !m.ActiveExperiments.Has(experiments.EphemeralValues) { + for _, oc := range m.Outputs { + if oc.EphemeralSet { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral values are experimental", + Detail: "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding ephemeral_values to the list of active experiments.", + Subject: oc.DeclRange.Ptr(), + }) + } + } + for _, vc := range m.Variables { + if vc.EphemeralSet { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral values are experimental", + Detail: "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding ephemeral_values to the list of active experiments.", + Subject: vc.DeclRange.Ptr(), + }) + } + } + } + return diags } diff --git a/internal/configs/module_merge.go b/internal/configs/module_merge.go index 01c3f2088c..42e956e5db 100644 --- a/internal/configs/module_merge.go +++ b/internal/configs/module_merge.go @@ -49,6 +49,10 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics { v.Sensitive = ov.Sensitive v.SensitiveSet = ov.SensitiveSet } + if ov.EphemeralSet { + v.Ephemeral = ov.Ephemeral + v.EphemeralSet = ov.EphemeralSet + } if ov.Default != cty.NilVal { v.Default = ov.Default } @@ -148,6 +152,10 @@ func (o *Output) merge(oo *Output) hcl.Diagnostics { o.Sensitive = oo.Sensitive o.SensitiveSet = oo.SensitiveSet } + if oo.EphemeralSet { + o.Ephemeral = oo.Ephemeral + o.EphemeralSet = oo.EphemeralSet + } // We don't allow depends_on to be overridden because that is likely to // cause confusing misbehavior. diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go index 43a624d9d8..ad82960fe3 100644 --- a/internal/configs/named_values.go +++ b/internal/configs/named_values.go @@ -35,9 +35,11 @@ type Variable struct { ParsingMode VariableParsingMode Validations []*CheckRule Sensitive bool + Ephemeral bool DescriptionSet bool SensitiveSet bool + EphemeralSet bool // Nullable indicates that null is a valid value for this variable. Setting // Nullable to false means that the module can expect this variable to @@ -120,6 +122,12 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno v.SensitiveSet = true } + if attr, exists := content.Attributes["ephemeral"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Ephemeral) + diags = append(diags, valDiags...) + v.EphemeralSet = true + } + if attr, exists := content.Attributes["nullable"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable) diags = append(diags, valDiags...) @@ -332,11 +340,13 @@ type Output struct { Expr hcl.Expression DependsOn []hcl.Traversal Sensitive bool + Ephemeral bool Preconditions []*CheckRule DescriptionSet bool SensitiveSet bool + EphemeralSet bool DeclRange hcl.Range } @@ -382,6 +392,12 @@ func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostic o.SensitiveSet = true } + if attr, exists := content.Attributes["ephemeral"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Ephemeral) + diags = append(diags, valDiags...) + o.EphemeralSet = true + } + if attr, exists := content.Attributes["depends_on"]; exists { deps, depsDiags := decodeDependsOn(attr) diags = append(diags, depsDiags...) @@ -473,6 +489,9 @@ var variableBlockSchema = &hcl.BodySchema{ { Name: "sensitive", }, + { + Name: "ephemeral", + }, { Name: "nullable", }, @@ -499,6 +518,9 @@ var outputBlockSchema = &hcl.BodySchema{ { Name: "sensitive", }, + { + Name: "ephemeral", + }, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "precondition"}, diff --git a/internal/configs/parser_config_test.go b/internal/configs/parser_config_test.go index c305cd1fab..e85bfa2da2 100644 --- a/internal/configs/parser_config_test.go +++ b/internal/configs/parser_config_test.go @@ -192,18 +192,27 @@ func TestParserLoadConfigFileWarning(t *testing.T) { sc := bufio.NewScanner(bytes.NewReader(src)) wantWarnings := make(map[int]string) lineNum := 1 + allowExperiments := false for sc.Scan() { lineText := sc.Text() if idx := strings.Index(lineText, marker); idx != -1 { summaryText := lineText[idx+len(marker):] wantWarnings[lineNum] = summaryText } + if lineText == "# ALLOW-LANGUAGE-EXPERIMENTS" { + allowExperiments = true + } lineNum++ } parser := testParser(map[string]string{ name: string(src), }) + // Some inputs use a special comment to request that they be + // permitted to use language experiments. We typically use that + // to test that the experiment opt-in is working and is causing + // the expected "you are using experimental features" warning. + parser.AllowLanguageExperiments(allowExperiments) _, diags := parser.LoadConfigFile(name) if diags.HasErrors() { diff --git a/internal/configs/testdata/warning-files/ephemeral_inputs_outputs.tf b/internal/configs/testdata/warning-files/ephemeral_inputs_outputs.tf new file mode 100644 index 0000000000..feedccd606 --- /dev/null +++ b/internal/configs/testdata/warning-files/ephemeral_inputs_outputs.tf @@ -0,0 +1,21 @@ +# ALLOW-LANGUAGE-EXPERIMENTS + +# If the ephemeral_values features get stabilized, this test input will fail +# due to the experiment being concluded, in which case it might make sense to +# move this file to valid-files and remove the experiment opt-in +# +# If this experiment is removed without stabilizing it then this will fail +# and should be removed altogether. + +terraform { + experiments = [ephemeral_values] # WARNING: Experimental feature "ephemeral_values" is active +} + +variable "in" { + ephemeral = true +} + +output "out" { + ephemeral = true + value = var.in +}