From 29435b46ac32296757da44e3674510b3097fcc62 Mon Sep 17 00:00:00 2001 From: Laura Pacilio <83350965+laurapacilio@users.noreply.github.com> Date: Wed, 8 Jun 2022 12:40:29 -0400 Subject: [PATCH 1/3] Edit type constraints docs for style and flow --- .../language/expressions/type-constraints.mdx | 73 ++++++------------- 1 file changed, 22 insertions(+), 51 deletions(-) diff --git a/website/docs/language/expressions/type-constraints.mdx b/website/docs/language/expressions/type-constraints.mdx index 5925b88472..10050e9130 100644 --- a/website/docs/language/expressions/type-constraints.mdx +++ b/website/docs/language/expressions/type-constraints.mdx @@ -260,11 +260,11 @@ value and thus perform no type conversion whatsoever. ## Optional Object Type Attributes -Terraform v1.3 adds support for marking particular attributes as optional in an -object type constraint. +-> **Note:** Optional type attributes are supported in Terraform v1.3 and later. -To mark an attribute as optional, use the additional `optional(...)` modifier -around its type declaration: +Terraform typically returns an error when it does not receive a value for specified object attributes. When you mark an attribute as optional, Terraform instead inserts a default value for the missing attribute. This allows the receiving module to describe an appropriate fallback behavior. + +To mark attributes as optional, use the `optional` modifier in the object type constraint. The following example creates optional attribute `b` and optional attribute with a default value `c`. ```hcl variable "with_optional_attribute" { @@ -276,29 +276,19 @@ variable "with_optional_attribute" { } ``` -When evaluating variable values, Terraform will return an error if an object -attribute specified in the variable type is not present in the given value. -Marking an attribute as optional changes the behavior in that situation: -Terraform will instead insert a default value for the missing attribute, -allowing the receiving module to describe an appropriate fallback behavior. +The `optional` modifier takes one or two arguments. +- **Type:** (Required) The first argument +specifies the type of the attribute. +- **Default:** (Optional) The second argument defines the default value that Terraform should use if the attribute is not present. This must be compatible with the attribute type. If not specified, Terraform uses a `null` value of the appropriate type as the default. -The `optional` modifier takes one or two arguments. The first argument -specifies the type of the attribute, and (if given) the second attribute -defines the default value to use if the attribute is not present. The default -must be compatible with the attribute type. If no default is specified, a -`null` value of the appropriate type will be used as the default. - -During evaluation, object attribute defaults are applied top-down in nested -variable types. This means that a given attribute's default value will also -have any nested default values applied to it later. +Terraform applies object attribute defaults top-down in nested variable types. This means that Terraform applies the default value you specify in the `optional` modifier first and then later applies any nested default values to that attribute. ### Example: Nested Structures with Optional Attributes and Defaults -The following configuration defines a variable which describes a number of storage buckets, each of which is used to host a website. This variable type uses several optional attributes, one of which is itself an `object` type with optional attributes and defaults. +The following example defines a variable for storage buckets that host a website. This variable type uses several optional attributes, including `website`, which is itself an optional `object` type that has optional attributes and defaults. ```hcl terraform { - # Optional attributes are currently experimental. experiments = [module_variable_optional_attrs] } @@ -315,8 +305,13 @@ variable "buckets" { } ``` -To test this out, we can create a file `terraform.tfvars` to provide an example -value for `var.buckets`: +The following example `terraform.tfvars` file specifies three bucket configurations for `var.buckets`. + +- `production` sets the routing rules to add a redirect +- `archived` uses default configuration, but is disabled +- `docs` overrides the index and error documents to use text files + +The `production` bucket does not specify the index and error documents, and the `archived` bucket omits the website configuration entirely. Terraform will use the default values specified in the `bucket` type constraint. ```hcl buckets = [ @@ -347,15 +342,11 @@ buckets = [ ] ``` -The intent here is to specify three bucket configurations: +This configuration produces the following variable values. -- `production` sets the routing rules to add a redirect; -- `archived` uses default configuration but is disabled; -- `docs` overrides the index and error documents to use text files. - -Note that `production` does not specify the index and error documents, and `archived` omits the website configuration altogether. Because our type specifies a default value for the `website` attribute as an empty object `{}`, Terraform fills in the defaults specified in the nested type. - -The resulting variable value is: +- For the `production` and `docs` buckets, Terraform sets `enabled` to `true`. Terraform also supplies default values for `website`, and then the values specified in `docs` override those defaults. +- For the `archived` and `docs` buckets, Terraform sets `routing_rules` to a `null` value. When Terraform does not receive optional attributes and there are no specified defaults, Terraform populates those attributes with a `null` value. +- For the `archived` bucket, Terraform populates the `website` attribute with the default values specified in the `buckets` type constraint. ```hcl tolist([ @@ -395,24 +386,4 @@ tolist([ } }, ]) -``` - -Here we can see that for `production` and `docs`, the `enabled` attribute has been filled in as `true`. The default values for the `website` attribute have also been filled in, with the values specified by `docs` overriding the defaults. For `archived`, the entire default `website` value is populated. - -One important point is that the `website` attribute for the `archived` and `docs` buckets contains a `null` value for `routing_rules`. When declaring a type constraint with an optional object attributes without a default, a value which omits that attribute will be populated with a `null` value, rather than continuing to omit the attribute in the final result. - -### Experimental Status - -Because this feature is currently experimental, it requires an explicit -opt-in on a per-module basis. To use it, write a `terraform` block with the -`experiments` argument set as follows: - -```hcl -terraform { - experiments = [module_variable_optional_attrs] -} -``` - -Until the experiment is concluded, the behavior of this feature may see -breaking changes even in minor releases. We recommend using this feature -only in prerelease versions of modules as long as it remains experimental. +``` \ No newline at end of file From 922de89be1b268843a612808b513afc3ca7bd311 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Mon, 13 Jun 2022 11:53:36 -0400 Subject: [PATCH 2/3] Conclude module variable optional attrs experiment --- internal/configs/experiments.go | 47 ------------------- .../object-optional-attrs-experiment.tf | 6 --- .../object-optional-attrs-experiment.tf | 6 --- internal/experiments/experiment.go | 3 +- internal/terraform/context_apply_test.go | 8 ---- 5 files changed, 2 insertions(+), 68 deletions(-) delete mode 100644 internal/configs/testdata/invalid-modules/object-optional-attrs-experiment/object-optional-attrs-experiment.tf rename internal/configs/testdata/{warning-files => valid-files}/object-optional-attrs-experiment.tf (70%) diff --git a/internal/configs/experiments.go b/internal/configs/experiments.go index 2ebf2d7006..8f330b6d9b 100644 --- a/internal/configs/experiments.go +++ b/internal/configs/experiments.go @@ -6,7 +6,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/version" - "github.com/zclconf/go-cty/cty" ) // When developing UI for experimental features, you can temporarily disable @@ -196,51 +195,5 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics { } */ - if !m.ActiveExperiments.Has(experiments.ModuleVariableOptionalAttrs) { - for _, v := range m.Variables { - if typeConstraintHasOptionalAttrs(v.ConstraintType) { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Optional object type attributes 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 module_variable_optional_attrs to the list of active experiments.", - Subject: v.DeclRange.Ptr(), - }) - } - } - } - return diags } - -func typeConstraintHasOptionalAttrs(ty cty.Type) bool { - if ty == cty.NilType { - // Weird, but we'll just ignore it to avoid crashing. - return false - } - - switch { - case ty.IsPrimitiveType(): - return false - case ty.IsCollectionType(): - return typeConstraintHasOptionalAttrs(ty.ElementType()) - case ty.IsObjectType(): - if len(ty.OptionalAttributes()) != 0 { - return true - } - for _, aty := range ty.AttributeTypes() { - if typeConstraintHasOptionalAttrs(aty) { - return true - } - } - return false - case ty.IsTupleType(): - for _, ety := range ty.TupleElementTypes() { - if typeConstraintHasOptionalAttrs(ety) { - return true - } - } - return false - default: - return false - } -} diff --git a/internal/configs/testdata/invalid-modules/object-optional-attrs-experiment/object-optional-attrs-experiment.tf b/internal/configs/testdata/invalid-modules/object-optional-attrs-experiment/object-optional-attrs-experiment.tf deleted file mode 100644 index 42dad02ab1..0000000000 --- a/internal/configs/testdata/invalid-modules/object-optional-attrs-experiment/object-optional-attrs-experiment.tf +++ /dev/null @@ -1,6 +0,0 @@ -variable "a" { - type = object({ - # The optional attributes experiment isn't enabled, so this isn't allowed. - a = optional(string) - }) -} diff --git a/internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf b/internal/configs/testdata/valid-files/object-optional-attrs-experiment.tf similarity index 70% rename from internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf rename to internal/configs/testdata/valid-files/object-optional-attrs-experiment.tf index 90bd8a6329..f0ff5cd07f 100644 --- a/internal/configs/testdata/warning-files/object-optional-attrs-experiment.tf +++ b/internal/configs/testdata/valid-files/object-optional-attrs-experiment.tf @@ -1,9 +1,3 @@ -terraform { - experiments = [ - module_variable_optional_attrs, # WARNING: Experimental feature "module_variable_optional_attrs" is active - ] -} - variable "a" { type = object({ foo = optional(string) diff --git a/internal/experiments/experiment.go b/internal/experiments/experiment.go index b5d67f58b3..48f92a32e2 100644 --- a/internal/experiments/experiment.go +++ b/internal/experiments/experiment.go @@ -27,7 +27,7 @@ func init() { registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.") registerConcludedExperiment(ConfigDrivenMove, "Declarations of moved resource instances using \"moved\" blocks can now be used by default, without enabling an experiment.") registerConcludedExperiment(PreconditionsPostconditions, "Condition blocks can now be used by default, without enabling an experiment.") - registerCurrentExperiment(ModuleVariableOptionalAttrs) + registerConcludedExperiment(ModuleVariableOptionalAttrs, "Optional object attributes in module variable type constraints can now be used by default, without enabling an experiment.") } // GetCurrent takes an experiment name and returns the experiment value @@ -92,6 +92,7 @@ var currentExperiments = make(Set) // Members of this map are registered in the init function above. var concludedExperiments = make(map[Experiment]string) +//lint:ignore U1000 No experiments are active func registerCurrentExperiment(exp Experiment) { currentExperiments.Add(exp) } diff --git a/internal/terraform/context_apply_test.go b/internal/terraform/context_apply_test.go index 97ee07769b..f1007906c1 100644 --- a/internal/terraform/context_apply_test.go +++ b/internal/terraform/context_apply_test.go @@ -11978,10 +11978,6 @@ resource "test_resource" "foo" { func TestContext2Apply_moduleVariableOptionalAttributes(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` -terraform { - experiments = [module_variable_optional_attrs] -} - variable "in" { type = object({ required = string @@ -12054,10 +12050,6 @@ output "out" { func TestContext2Apply_moduleVariableOptionalAttributesDefault(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` -terraform { - experiments = [module_variable_optional_attrs] -} - variable "in" { type = object({ required = string From 8db2552b5f8b229afa96aa3b961b07a514a994ec Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Fri, 17 Jun 2022 11:38:12 -0400 Subject: [PATCH 3/3] Remove experiment from optional attrs example --- website/docs/language/expressions/type-constraints.mdx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/website/docs/language/expressions/type-constraints.mdx b/website/docs/language/expressions/type-constraints.mdx index 10050e9130..d8b7627210 100644 --- a/website/docs/language/expressions/type-constraints.mdx +++ b/website/docs/language/expressions/type-constraints.mdx @@ -288,10 +288,6 @@ Terraform applies object attribute defaults top-down in nested variable types. T The following example defines a variable for storage buckets that host a website. This variable type uses several optional attributes, including `website`, which is itself an optional `object` type that has optional attributes and defaults. ```hcl -terraform { - experiments = [module_variable_optional_attrs] -} - variable "buckets" { type = list(object({ name = string @@ -386,4 +382,4 @@ tolist([ } }, ]) -``` \ No newline at end of file +```