hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.36k stars 9.49k forks source link

Optional arguments in object variable type definition #19898

Closed prehor closed 2 years ago

prehor commented 5 years ago

Current Terraform Version

Terraform v0.12.0-alpha4 (2c36829d3265661d8edbd5014de8090ea7e2a076)

Proposal

I like the object variable type and it would be nice to be able to define optional arguments which can carry null value too, to use:

variable "network_rules" {
  default = null
  type = object({
    bypass = optional(list(string))
    ip_rules = optional(list(string))
    virtual_network_subnet_ids = optional(list(string))
  })
}

resource "azurerm_storage_account" "sa" {
  name = random_string.name.result
  location = var.location
  resource_group_name = var.resource_group_name
  account_replication_type = var.account_replication_type
  account_tier = var.account_tier

  dynamic "network_rules" {
    for_each = var.network_rules == null ? [] : list(var.network_rules)

    content {
      bypass = network_rules.value.bypass
      ip_rules = network_rules.value.ip_rules
      virtual_network_subnet_ids = network_rules.value.virtual_network_subnet_ids
    }
  }

instead of:

variable "network_rules" {
  default = null
  type = map(string)
}

resource "azurerm_storage_account" "sa" {
  name = random_string.name.result
  location = var.location
  resource_group_name = var.resource_group_name
  account_replication_type = var.account_replication_type
  account_tier = var.account_tier

  dynamic "network_rules" {
    for_each = var.network_rules == null ? [] : list(var.network_rules)

    content {
      bypass = lookup(network_rules, "bypass", null) == null ? null : split(",", lookup(network_rules, "bypass"))
      ip_rules = lookup(network_rules, "ip_rules", null) == null ? null : split(",", lookup(network_rules, "ip_rules"))
      virtual_network_subnet_ids = lookup(network_rules, "virtual_network_subnet_ids", null) == null ? null : split(",", lookup(network_rules, "virtual_network_subnet_ids"))
    }
  }
}
robbruce commented 4 years ago

TypeScript has a good way of declaring a property on an interface as optional

interface MyObject {
  id:    string;
  name?: string;
}

The ? make the name property optional. Maybe simple way to declare this for HCL could be

object({
  id    = string
  name? = string
})
aaronsteers commented 4 years ago

What about adding support for "configuration blocks" in modules just like we can do for resources.

In a resouce, I can do this:

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "TLS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "ssh"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

And each block can optionally be specified multiple times, each can have optional params, and each on the backend can interact with or leverage the defaults of the underlying config.

Is there any plans to move modules in this direction as well?

(Returning again to this issue after a while, as it is becoming another development roadblock as I migrate everything to TF 0.13 and as we can now have modules-calling-a-dynamic-count-of-modules.)

Whether implemented by 'objects' or something else on the backend, the challenge (at least the one I keep running into) is to "have many of a thing" in a module while still only needing "just enough" config to get the job done.

aaronsteers commented 4 years ago

Please watch and vote for this related issue if it would resolve your use case: https://github.com/hashicorp/terraform/issues/26051

In short:

variable "ingress" {
  type = block

  # Nested variables okay within 'block' variable type
  variable "from_port" {
    type = int
  }
  variable "to_port" {
    type = int
  }
  variable "description" {
    type    = string
    default = null # optional variables work just as they do today
  }
}

Which would function exactly how configuration blocks already do today for resources (but not for modules):

module "mymodule" {
  source = "..."
  ingress {
     from_port = 880
     to_port =  880
  }
  ingress {
     description = "Totally optional."
     from_port = 22
     to_port =  22
  }
}

Terraform would still output a list of strongly-typed objects, but because terraform is parsing the defaults block-by-block, each value in can be optional while still having the deterministic schema needed by objects today.

Inspired in part by @wdec and @michelzanini 's comments above.

dylanturn commented 3 years ago

Could something like this work?

variable "helm_parameters" {
  type        = list(object({
    name: string,
    value: any,
    force_string: bool,
  },{
    name: "default_name_string_or_null",
    value: "default_name_string_or_null",
    force_string: false
  }))
  description = "Parameters that will override helm_values"
  default     = []
}
luanhdeandrade commented 3 years ago

Will this still be addressed ?

ocervell commented 3 years ago

+1 it's been more than 2 years, this prevents creating complex variable types with Terraform (e.g: list of maps with optional fields, or maps with optional fields and more than 1 type within the map ...)

jefftucker commented 3 years ago

I've found a workaround for this, tested in v0.12.x but I don't see why it wouldn't work for v0.11.x also and probably v0.13x. This is EXTREMELY ugly depending on the complexity of your input, but it works and I'm using it so that I can specify only the data I need in an input variable somewhere instead of having to fill out every last field. It works for nested objects as well, but it gets uglier for each layer. The trick is that Terraform is happy to accept "any" as a type, and then the merge() function works both with maps and with objects. Imagine I have a module called "account" that has an input called "users" defined as such:

# account/variables.tf
variable "users" {
     type = map(object({
                email = string, # I want this to be required and get an error if it's missing
                readonly = bool, # I want this to default to false
                enabled = bool # I want this to default to true
               }))
}

Now for each user, I don't want to specify anything besides the email (the key will be their username) unless I need to, so my input in my inputs.auto.tfvars file would look like this:

users = { jsnuffy = {email = "jsnuffy@example.com},
                joeyjojo = {email = "joeyjojojr@example.com", readonly = true}
} 

Ok, now here's the fun part. I need to "fix up" the missing stuff with some defaults from somewhere so that the module gets the correct types and doesn't complain and/or crash Terraform (I've found a few dozen ways to crash it in working on this). My root module looks like this:

# main.tf
variable "users" {
    type = any,  # this is really critical or it'll try to coerce your input into something that you don't want
   default = {}  # this is optional; require the value or don't.
}

# hold my beer
locals {
    # where's the default for the input?  Let's just put it here!  
    defaultUser = {
         readonly = false,
         enabled = true
    }

    # THE ORDER HERE MATTERS!  the default comes first in the merge() function
    actual_users = { for k, v in var.users:                       
                        k => merge(local.defaultUser, v)
                  }
}

# now, local.actual_users has the right fields since if they were missing, the defaultUser variable had them and anything
# overwritten in whatever happens to be in the var.users input will be overwritten from the merge() function here.
# This is what I can pass to the module:

module "accounts" {
    src = "path/to/accounts"
    # this has been fixed up, so everything will match the type 
    # UNLESS the email is missing since we didn't include that with the default object that we passed to merge()
    users = local.actual_users 

}

That's all there is to it. If you have a nested value, you do the same thing but introduce two more intermediary variables. The first has the defaults for everything that is not nested. The second takes the keys for all the inputs and declares ONLY the nested module, then does another loop calling merge() on the key/values in the nested input (use lookup() if you want that itself to be optional). Now you have an intermediary local with all the fields that aren't nested, another with only the nested field, and then you can call merge() on these two locals to get your final local to pass in to the module. You could probably abstract the locals by using a data-only module to fix up the inputs and make this slightly more clean, but the basic idea is to declare the input you actually want and then declare the "default" values elsewhere, then merge the input you get with the default values to construct a valid input that is then passed in to whatever you need to use. Hope this helps someone; it took me a long time to find a way to make this work.

itsankoff commented 3 years ago

Guys I just found a working workaround for having great control over variables and their default values for composite type for Terraform 0.13.x. The solution uses the new variable validation approach. The workaround fixes 2 major limitations of Terraform:

  1. Having a list of objects with different structure and still having type check
  2. Having a way to specify optional properties in the list's objects

Here is a brief description:

Before we start with the code, here is some context. I want to define ingress rules for an AWS security_group module. The internal aws_security_group resource has 3 options for the source of the ingress rules of a security group - cidr_blocks = list(string), source_security_group_id = string and self = bool. Now I want to have a unified structure on how to supply all the ingress rules from the module. It should look something like this:

module "security_group" "test" {
    source = "/path/to/module"

    vpc_id = "<vpc_id>"
    name = "test"

    ingress = [
        {
            protocol = "tcp"
            from_port = 0
            to_port = 65535
            description = "test sec group"
            source = {
                cidr_blocks = ["0.0.0.0/0"]
            }
        },
        {
            protocol = "tcp"
            from_port = 0
            to_port = 65535
            description = "test sec group"
            source = {
                source_security_group_id = "sg-<id>"
            }
        },
        {
            protocol = "tcp"
            from_port = 0
            to_port = 65535
            description = "test sec group"
            source = {
                 self = true
            }
        }
    ]
}

Now if I tried to define the input variable of the module like this:

variable "ingress" {
    type = string(object{
        protocol = string
        from_port = number
        to_port = number
        description = string
        source = any
     })
}

In theory, this should work. BUTTT it doesn't. The Terraform requires all objects within a list to look exactly the same. So, source = any gives us the flexibility, but the list limits us to have either all elements of the list to be with source = { cidr_blocks = [...] } or source = { self = true|false } and the same for source_security_group_id.

Can we do any better?

To bypass the list limitation we can use type = any for the whole input variable. But then we will lose the type checks during plan time which can result in misconfigurations during apply time.

So, can we do any better?

HOLD MY BEER!

# variables.tf
# This is a variable for a list of ingress rules for an AWS security_group module
variable "ingress" {
    type = any # we need this because Terraform requires objects to be absolutely equal for lists
    default = []    

    # we will use the new validation feature in 0.13 to do type check during plan time
    validation = {
        # The ternary operator returns true if the list is empty, because we want to support egress only security groups
        condition = length(var.ingress) > 0 ? length([
            for x in var.ingress : x
            if lookup(x, "protocol", null) != null &&
            if lookup(x, "from_port", null) != null &&
            if lookup(x, "source", null) != null        
        ]) == length(var.ingress) : true
        error_message = "Ingress rules validation error!"
    }
}

So let's see how this works. First, we define the type of ingress variable to be any. This removes the list limitation of object structure equality. But we want to have a loose type of validation. So what we do:

  1. If the list is empty we return true so the validation can pass
  2. If the list is not empty, then we filter all the objects that have all the required fields available. If there are rules that do not have any of the required fields (e.g. "protocol") then they will be missing from the result list.
  3. In the end we check whether the filtered list has the same length as the provided list. If the length is not equal this means that we filtered an invalid rule from the list

A side effect of this approach is that we can still have type validation, but validate only the required fields and omit validation of optional fields like the description of the rules in our case. As you can see we don't lookup for the description field which means that we can provide it optionally.

I hope this can be of help until Terraform provides a more robust mechanism for type definition and type reuse.

cc @jefftucker @ocervell

jValdron commented 3 years ago

I don't have time to try it myself, but instead of trying workarounds and all, you guys should just try out v0.14.0-rc1. They've released this new experimental feature:

module_variable_optional_attrs: When declaring an input variable for a module whose type constraint (type argument) contains an object type constraint, the type expressions for the attributes can be annotated with the experimental optional(...) modifier. Marking an attribute as "optional" changes the type conversion behavior for that type constraint so that if the given value is a map or object that has no attribute of that name then Terraform will silently give that attribute the value null, rather than returning an error saying that it is required. The resulting value still conforms to the type constraint in that the attribute is considered to be present, but references to it in the recieving module will find a null value and can act on that accordingly.

itsankoff commented 3 years ago

This sounds interesting, I will check it out. But still, the list limitation for object structural equality is not resolved

lperdereau commented 3 years ago

This is really good. I think optional function should take a second parameter for default value.

For exemple I have this variable :

variable "ports" {
  type = list(object({
    name = string
    network_id = string
    subnet_id = string
    admin_state_up = optional(bool)
    security_group_ids = optional(list(string))
    ip_address = optional(string)
  }))
  description = <<EOF
The ports list
EOF
  default = []
}

For a list object it could be interesting to have a default value for optional attributes without a locals and defaults function like here

Something like this:

variable "ports" {
  type = list(object({
    name = string
    network_id = string
    subnet_id = string
    admin_state_up = optional(bool, true)
    security_group_ids = optional(list(string), ["general", "ssh"])
    ip_address = optional(string, null)
  }))
  description = <<EOF
The ports list
EOF
  default = []
}
thiagolsfortunato commented 3 years ago

I found this documentation with the experimental function defaults being used with optional.

https://www.terraform.io/docs/configuration/functions/defaults.html

This is not the best implementation, because you need to control default values at locals., but we can set default values for modules, We just need to write good documentation.

Just for: 0.15+

ArchiFleKs commented 3 years ago

I just tested this, I have an issue with optional values being set to null, is there a way to get them omited ?

My use case is this, on a majority of my module I use this pattern :

  aws-load-balancer-controller = merge(
    local.helm_defaults,
    {
      name                      = "aws-load-balancer-controller"
      namespace                 = "aws-load-balancer-controller"
      chart                     = "aws-load-balancer-controller"
      repository                = "https://aws.github.io/eks-charts"
      service_account_name      = "aws-load-balancer-controller"
      create_iam_resources_irsa = true
      enabled                   = false
      chart_version             = "1.0.5"
      version                   = "v2.0.0"
      iam_policy_override       = null
      default_network_policy    = true
      allowed_cidrs             = ["0.0.0.0/0"]
    },
    var.aws-load-balancer-controller

For now my variable aws-load-balancer-controller is type any with default to {}. This allow the module to work with just:

aws-load-balancer-controller = {
  enabled = true
}

Without further customization, if I set a validation like that:

variable "aws-ebs-csi-driver" {                                                      
+   description = "Customize aws-ebs-csi-driver helm chart, see `aws-ebs-csi-driver.tf`"    
+   type = object({                                                                    
+     atomic                = optional(bool)                                           
+     cleanup_on_fail       = optional(bool)                                           
+     dependency_update     = optional(bool)                                           
+     disable_crd_hooks     = optional(bool)                                           
+     disable_webhooks      = optional(bool)                                           
+     force_update          = optional(bool)                                           
+     recreate_pods         = optional(bool)                                           
+     render_subchart_notes = optional(bool)                                           
+     replace               = optional(bool)                                           
+     reset_values          = optional(bool)                                           
+     reuse_values          = optional(bool)                                           
+     skip_crds             = optional(bool)                                           
+     timeout               = optional(number)                                         
+     verify                = optional(bool)                                           
+     wait                  = optional(bool)                                           
+     extra_values          = optional(string)                                         
+     name                  = optional(string)                                         
+     namespace             = optional(string)                                         
+     chart                 = optional(string)                                         
+     repository            = optional(string)                                         
+     service_account_names = optional(object({                                        
+       controller = optional(string)                                                  
+       snapshot   = optional(string)                                                  
+     }))                                                                              
+     create_iam_resources_irsa = optional(bool)                                       
+     create_storage_class      = optional(bool)                                       
+     storage_class_name        = optional(string)                                     
+     is_default_class          = optional(bool)                                       
+     enabled                   = optional(bool)                                       
+     chart_version             = optional(string)                                     
+     version                   = optional(string)                                     
+     iam_policy_override       = optional(string)                                     
+     default_network_policy    = optional(bool)                                       
+   })                                                                                 
+   default = {}                                                                       
+ }                        

It remplace the variable with null and the merge use null as a value as the merge is done from local to the vars in that order.

I don't really need to enforce validation I'm trying to use this feature only for documentation but maybe there is some other way

fayak commented 3 years ago

One solution I found was to use YAML as source for variables. For example, for a list of servers:

servers.yml

server: &default
  cpu: 8
  memory: 8192
  ips: []

servers:
  server1:
    <<: *default
    cpu: 2
    ips: ["10.2.8.124"]
  server2:
    <<: *default
    cpu: 4

main.tf

locals {
    servers = yamldecode(file("servers.yml")).servers
}

Each server in variable servers will contain the 3 attributes defined, either the default or the overridden ones. Again, maybe not as good as implemented directly in terraform I reckon, but at least it works for us

thiagolsfortunato commented 3 years ago

I'm try to use optionals() and defaults() on object type but return error:

$ terraform -v
Terraform v0.14.6
+ provider registry.terraform.io/hashicorp/aws v3.29.0

variables.tf

variable "cert" {
  description = "ACM Certificate Attributes"
  type = object({
    domain_name               = string
    subject_alternative_names = list(string)
    validation_method         = string
    tags                      = map(string)

    options = optional(object({
      certificate_transparency_logging_preference = optional(string)
    }))
  })
}

main.tf

locals {
  cert = defaults(var.cert, {
    options = {
      certificate_transparency_logging_preference = ""
    }
  })
}

resource "aws_acm_certificate" "cert" {
  domain_name       = local.cert.domain_name
  validation_method = local.cert.validation_method

  subject_alternative_names = local.cert.subject_alternative_names

  dynamic "options" {
    for_each = local.cert.options.certificate_transparency_logging_preference == "" ? [] : [var.cert.options]

    content {
      certificate_transparency_logging_preference = options.value.certificate_transparency_logging_preference
    }
  }

  tags = local.cert.tags
}

module.tf

module "my-cert" {
  source = "../../../../tf-module-certificate-manager"

  cert = {
    domain_name               = "example.com"
    validation_method         = "DNS"
    subject_alternative_names = ["*.example.com"]

    tags = {
      Name = "example.com"
    }

    # options = {
    #   certificate_transparency_logging_preference = "ENABLED"
    # }
  }
}

This configuration return the follow error:

Error: Error in the function call

  on ../../../../tf-module-certificate-manager/main.tf line 37, in locals:
  37:   cert = defaults(var.cert, {
  38:     options = {
  39:       certificate_transparency_logging_preference = ""
  40:     }
  41:   })
    |----------------
    | var.cert is object with 5 attributes

Call to function "defaults" failed: panic in function implementation:
interface conversion: interface {} is nil, not map[string]interface {}
goroutine 10177 [running]:
runtime/debug.Stack(0xc004349268, 0x24f9dc0, 0xc004358a80)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9f
github.com/zclconf/go-cty/cty/function.errorForPanic(...)
        /go/pkg/mod/github.com/zclconf/go-cty@v1.7.1/cty/function/error.go:44
github.com/zclconf/go-cty/cty/function.Function.Call.func1(0xc00434a2f0,
0xc00434a310)
        /go/pkg/mod/github.com/zclconf/go-cty@v1.7.1/cty/function/function.go:291
+0x95
panic(0x24f9dc0, 0xc004358a80)
        /usr/local/go/src/runtime/panic.go:969 +0x1b9
github.com/zclconf/go-cty/cty.Value.GetAttr(0x2ce7960, 0xc001852b50, 0x0, 0x0,
0xc00098f5f0, 0x2b, 0x0, 0x0, 0x2ce7860, 0xc0000503d9)
        /go/pkg/mod/github.com/zclconf/go-cty@v1.7.1/cty/value_ops.go:755 +0x40b
github.com/hashicorp/terraform/lang/funcs.defaultsApply(0x2ce7960,
0xc001852b50, 0x0, 0x0, 0x2ce7960, 0xc0025df9e0, 0x24ba300, 0xc004358750,
0x24ba300, 0xc004358750, ...)
        /home/circleci/project/project/lang/funcs/defaults.go:102 +0x385
github.com/hashicorp/terraform/lang/funcs.defaultsApply(0x2ce7960,
0xc0025df910, 0x24ba300, 0xc004358360, 0x2ce7960, 0xc0025df9f0, 0x24ba300,
0xc0043587b0, 0x0, 0xc00434a0d8, ...)
        /home/circleci/project/project/lang/funcs/defaults.go:107 +0x45c
github.com/hashicorp/terraform/lang/funcs.glob..func29(0xc004304a40, 0x2, 0x2,
0x2ce7960, 0xc0025df910, 0xc0025dfb90, 0x24ba300, 0xc0043589c0, 0xc004358a20,
0xc00434a190, ...)
        /home/circleci/project/project/lang/funcs/defaults.go:65 +0xb3
github.com/zclconf/go-cty/cty/function.Function.Call(0xc00021a990,
0xc004304a40, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
        /go/pkg/mod/github.com/zclconf/go-cty@v1.7.1/cty/function/function.go:295
+0x51a
github.com/hashicorp/hcl/v2/hclsyntax.(*FunctionCallExpr).Value(0xc00104b0e0,
0xc0019ff0a0, 0x0, 0xc00434b800, 0x1, 0x1, 0x0, 0x0, 0x0)
        /go/pkg/mod/github.com/hashicorp/hcl/v2@v2.8.2/hclsyntax/expression.go:442
+0x10c5
github.com/hashicorp/terraform/lang.(*Scope).EvalExpr(0xc003f4efa0, 0x2ce63e0,
0xc00104b0e0, 0x2ce78a0, 0x3dfc420, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        /home/circleci/project/project/lang/eval.go:171 +0x1b7
github.com/hashicorp/terraform/terraform.(*BuiltinEvalContext).EvaluateExpr(0xc003ea9ee0,
0x2ce63e0, 0xc00104b0e0, 0x2ce78a0, 0x3dfc420, 0x0, 0x0, 0x0, 0x200000003,
0xc0015b2000, ...)
        /home/circleci/project/project/terraform/eval_context_builtin.go:287 +0xbb
github.com/hashicorp/terraform/terraform.(*NodeLocal).Execute(0xc003eccab0,
0x2d23580, 0xc003ea9ee0, 0xc0035a0002, 0x2509020, 0x2690320)
        /home/circleci/project/project/terraform/node_local.go:156 +0x71d
github.com/hashicorp/terraform/terraform.(*ContextGraphWalker).Execute(0xc0031a2c30,
0x2d23580, 0xc003ea9ee0, 0x7fe67a2bd428, 0xc003eccab0, 0x0, 0x0, 0x0)
        /home/circleci/project/project/terraform/graph_walk_context.go:127 +0xbc
github.com/hashicorp/terraform/terraform.(*Graph).walk.func1(0x2690320,
0xc003eccab0, 0x0, 0x0, 0x0)
        /home/circleci/project/project/terraform/graph.go:59 +0x962
github.com/hashicorp/terraform/dag.(*Walker).walkVertex(0xc000d91140,
0x2690320, 0xc003eccab0, 0xc002cc9e40)
        /home/circleci/project/project/dag/walk.go:387 +0x375
created by github.com/hashicorp/terraform/dag.(*Walker).Update
        /home/circleci/project/project/dag/walk.go:309 +0x1246
.

Releasing state lock. This may take a few moments...

Someone could help me?

ktmorgan commented 3 years ago

I'm try to use optionals() and defaults() on object type but return error:

$ terraform -v
Terraform v0.14.6
+ provider registry.terraform.io/hashicorp/aws v3.29.0

Someone could help me?

@thiagolsfortunato According to the doc, defaults is only available in 0.15+

https://www.terraform.io/docs/language/functions/defaults.html

thiagolsfortunato commented 3 years ago

I'm try to use optionals() and defaults() on object type but return error:

$ terraform -v
Terraform v0.14.6
+ provider registry.terraform.io/hashicorp/aws v3.29.0

Someone could help me?

@thiagolsfortunato According to the doc, defaults is only available in 0.15+

https://www.terraform.io/docs/language/functions/defaults.html

@ktmorgan but it's available as experimental feature.

https://www.terraform.io/docs/language/expressions/type-constraints.html#experimental-optional-object-type-attributes

terraform { 
   experiments = [module_variable_optional_attrs] 
}
whiskeysierra commented 3 years ago

The docs are not very clear on this, but I tried pretty much everything and I believe that you can't have optional structural types due to how the defaults functions treats those. It will basically try to get to the deepest level of your data structure before applying defaults.

What I ended up with was having very shallow objects and instead of defaults I'm using coalesce on each key/property individually. That works with optional objects/lists/etc at least.

DenWin commented 3 years ago

Does there exist any timeline as to when "module_variable_optional_attrs" - and defaults - won't be experimental anymore?

thiagolsfortunato commented 3 years ago

Does there exist any timeline as to when "module_variable_optional_attrs" - and defaults - won't be experimental anymore?

@DenWin when launching the new release 0.15.

DenWin commented 3 years ago

Does there exist any timeline as to when "module_variable_optional_attrs" - and defaults - won't be experimental anymore?

@DenWin when launching the new release 0.15.

Sorry, but they still seem to be experimental.

ericsonrumuy7 commented 3 years ago

Does there exist any timeline as to when "module_variable_optional_attrs" - and defaults - won't be experimental anymore?

@DenWin when launching the new release 0.15.

Sorry, but they still seem to be experimental.

Any update on this ? really need this feature :)

SavvasRonTruex commented 3 years ago

At this moment even if optional would be added that would be a huge step, there are other ways to handle default.

vistpp commented 3 years ago

Maybe someone could provide a likelihood this experiment will go to production? Really need it, and we are wondering about the risk using this experimental feature in production.

grimm26 commented 3 years ago

I'm using it in production in a couple modules. You're get the annoying warning, but that's not too troublesome. They'll promote it I'm sure.

laughtonsm commented 3 years ago

Same as @grimm26 here. It's proving very useful, although the try function seems to not handle the optional properties too well. Or it could just be me not using it correctly

sidh commented 3 years ago

Yep, optional is very useful. It simplifies our code a lot.

speller commented 3 years ago

The optional function works well. It also would be nice to have an additional parameter to define a default value instead of null.

mponton-cn commented 3 years ago

Adding my 👍🏻 on optional and the module_variable_optional_attrs flag. We've been using it for a while in our modules and I'm getting anxious to see this promoted to a real non-experimental feature. A rollback/removal of the feature would make me pretty sad...

alex-feel commented 3 years ago

Same as @grimm26 here. It's proving very useful, although the try function seems to not handle the optional properties too well. Or it could just be me not using it correctly

It works well for me.

Roxyrob commented 3 years ago

Optional is a great feature that simplify a lot the code but as others already said, alongside default null behavior, the ability to define a custom default:

argumentN optional(bool, true)

will really complete "optional" feature greatly improving DRY and allowing a single point for variables/arguments declaretion instead of duplication in variables + locals.

md5 commented 3 years ago

Optional is a great feature that simplify a lot the code but as others already said, alongside default null behavior, the ability to define a custom default:

argumentN optional(bool, true)

will really complete "optional" feature greatly improving DRY and allowing a single point for variables/arguments declaretion instead of duplication in variables + locals.

It's more than a matter of duplication. Having the default declared inline as part of the variable declaration allows documentation to be generated automatically without the risk of the default and locals getting out of sync.

lupass93 commented 2 years ago

indispensable and fantastic functionality! as they have implemented it now! I hope that from the next version the warning of the experimental functionality will disappear and become stable! I use it in every module! I can’t help it! totally in agreement with @prehor

foogod commented 2 years ago

From what I can tell, this feature has been in the "experimental" phase for approaching a year now. When will this be promoted to a non-experimental feature?

optional() is potentially very useful for what I want to do, but I can't actually use it because my organization is (understandably) wary of relying on experimental features in a production environment. The fact that it has apparently been working fine as an experiment for a long time but nobody has bothered to make it "live" is, frankly, rather frustrating at this point.

I would also personally support the idea of adding a second parameter to the optional() syntax to specify a default value other than null (I think the defaults() stuff is overly cumbersome for most cases and unnecessarily separates default values from the rest of the schema definition, TBH), but frankly I'd be perfectly happy if the current behavior was just made non-experimental as-is.

Just give us something we can actually justify using in real applications (without all the scary warning messages), please!

GoodMirek commented 2 years ago

This is really useful functionality, will love to see it promoted too.

marceloboeira commented 2 years ago

I think the defaults() stuff is overly cumbersome for most cases and unnecessarily separates default values from the rest of the schema definition, TBH), but frankly I'd be perfectly happy if the current behavior was just made non-experimental as-is. @foogod

I guess they are different features at the end, I see use for both, but totally agree that trying to use defaults to cover the optional behavior is just... ugly.

amarkevich commented 2 years ago

@hashicorp Take 1.5+k votes into account during next feature planning plz

morphalus commented 2 years ago

I wanted to set optional attributes so I landed on that page, so one more comment for asking an implementation of module_variable_optional_attrs no more experimental :)

jonwtech commented 2 years ago

To be honest I use module_variable_optional_attrs so extensively now that if it's removed I'll just have to stay on my current version of TF forever :) - it's been a godsend. Agree that an optional second parameter for a default value would make it perfect. The current defaults function doesn't behave intuitively for me when dealing with maps and lists.

code-haven commented 2 years ago

optional + defaults has great potential for module developers to express complex & nested inputs that's possible in many yaml/json based templating engines(helm etc).

Vinayaks439 commented 2 years ago

Hey Guys! I'm facing similar issue when creating optional variables inside a list of objects. Is this Issue related to that or did I missing something in the comments for workarounds.

terraform version: 0.13.7

variable "foo" { type = list(object({ bar= optional(bool), hello= optional(bool), bye= optional(bool), name = string, description = string, birthyear = optional(number), age = optional(number), text = string, records= list(object({ name = string description = string alias = string })) })) }

Issue: Keyword "optional" is not a valid type constructor.

jonwtech commented 2 years ago

Hey Guys! I'm facing similar issue when creating optional variables inside a list of objects. Is this Issue related to that or did I missing something in the comments for workarounds.

terraform version: 0.13.7

variable "foo" { type = list(object({ bar= optional(bool), hello= optional(bool), bye= optional(bool), name = string, description = string, birthyear = optional(number), age = optional(number), text = string, records= list(object({ name = string description = string alias = string })) })) }

Issue: Keyword "optional" is not a valid type constructor.

You need to be on at least v0.14

gmauleon commented 2 years ago

Another 2022 bump on this, would definitely need it in my map of object setup where I don't want advanced options to be mandatory.

prowlaiii commented 2 years ago

I find the separate declaration of the types and the defaults a bit cumbersome and the "optional" keyword superfluous.

I think this has already been suggested, but the syntax for declaring the attributes of a variable is already defined, so it would be consistent to just make them recursive, eg.

variable "simple1" {
  default     = "blah"
  description = "A simple variable"
  type        = string
}
variable "complex1" {
  default = null
  description = "A complex variable"
  type = object({
    mystring1 = {
      default = "blah"
      description = "A string in an object variable (optional)"
      type = string
    }
    mystring2 = {
      description = "A string in an object variable (required)"
      sensitive = true
      type = string
    }
    mylist1 = {
      default = []
      description = "A list in an object variable (optional)"
      type = list(string)
    }
    mymap1 = {
      default = {}
      description = "A map in an object variable (optional)"
      type = map(string)
    }
    myobject1 = {
      default = null
      description = "An object within an object variable (optional)"
      type = object({
        ....
      )}
    }
    mystring3 = { description = "Another string in an object variable (required)", type = string }
  })
}
ImIOImI commented 2 years ago

I was surprised that when specifying an object as a type it filters out all inputs that aren't declared. I love all the ideas about specifying optional variables and stuff... but this has been open for 4 years. Maybe just don't kill things not declared in the type, then give us the awesome later?

tfvars:

test = {
  map0 = {
    required = "string"
  }
  map1 = {
    required = "string"
    optional = "string"
  }
}

variable:

variable "test" {
  type = map(
    object(
      {
        required = string
      }
    )
  )
}

outputs:

test = tomap({
  "map0" = {
    "required" = "string"
  }
  "map1" = {
    "required" = "string"
  }
})

In my opinion this should either error or let optional go through. I'm 100% in favor of making this declared explicitly, but it's current behavior is very surprising.

pascal-hofmann commented 2 years ago

I was surprised that when specifying an object as a type it filters out all inputs that aren't declared. I love all the ideas about specifying optional variables and stuff... but this has been open for 4 years. Maybe just don't kill things not declared in the type, then give us the awesome later?

I prefer to have them declared explicitly. This way you can look at the type declaration and see which fields are supported.

dylanturn commented 2 years ago

I prefer to have them declared explicitly. This way you can look at the type declaration and see which fields are supported.

It also means that the language server (terraform-ls/terraform-lsp) can introspect the modules and provide much more accurate auto-complete suggestions...

ImIOImI commented 2 years ago

I prefer to have them declared explicitly. This way you can look at the type declaration and see which fields are supported.

Me too... but the five stages of grief are "denial, anger, bargaining, depression and acceptance" and right now I'm at the bargaining stage. If I can't have something perfect, can I at least have something less nice?

mponton-cn commented 2 years ago

I'm at the bargaining phase myself. Simply getting rid of the "experimental" warning would please me.

tikicoder commented 2 years ago

For me this has been experimental for how long, I just would like to know what is the reason, what is causing issues, and is it really something that should be a blocker versus a hey every FYI this WILL break if you do this you know it will break.