I'm using Terraform to manage a Github organization. We have a standard "common repository" module that we use to ensure our repositories share a common configuration. I would like to add support for configuring GitHub pages, which requires support for the pages
element, which looks like this:
pages {
build_type = "legacy"
cname = "example.com"
source {
branch = "master"
path = "/docs"
}
}
Everything is optional. In particular, source
is only required if build_type == "legacy" || build_type == null
, and the entire pages
block can be omitted. I couldn't figure out how to make source
conditional, so I ended up splitting this into two dynamic
blocks like this:
# There are two `dynamic "pages"` blocks here to account for the fact that `source` is only required
# if `build_type` is "legacy". The `for_each` at the top of each block will only enable the block when
# the necessary conditions are met.
dynamic "pages" {
# enable this block if `pages` is not null and `build_type` is "legacy" (or null)
for_each = var.pages == null ? [] : var.pages.build_type == "legacy" || var.pages.build_type == null ? ["enabled"] : []
content {
source {
branch = var.pages.source.branch
path = var.pages.source.path
}
cname = var.pages.cname
build_type = var.pages.build_type
}
}
dynamic "pages" {
# enable this block if `pages` is not null and `build_type` is "workflow"
for_each = var.pages == null ? [] : var.pages.build_type == "workflow" ? ["enabled"] : []
content {
cname = var.pages.cname
build_type = var.pages.build_type
}
}
Where I've defined the pages
variable in the module like this:
variable "pages" {
description = "Configuration for github pages"
type = object({
source = optional(object({
branch = string
path = string
}))
build_type = optional(string, "legacy")
cname = optional(string)
})
default = null
}
Is there a better way to approach this?
Your input variable declaration already ensures that
build_type
itself cannot be null, since Terraform will use the default value"legacy"
instead if so. I would suggest also adding a validation rule to enforce thatsource
can only be set ifbuild_type
is"legacy"
, like this:With that in place you can safely assume elsewhere in the module that
var.pages.source
will always benull
unlessvar.pages.build_type
is"legacy"
, andvar.pages.build_type
can never benull
.I think at that point the rest of the problem reduces to: declare the
pages
block only ifvar.pages
is not null, and declare thesource
block withinpages
only ifvar.pages.source
is not null, which can be written like this:Applying the
[*]
operator to a value that isn't of a list, tuple, or set type causes Terraform to return a tuple with either zero or one elements depending on whether the operand isnull
. This treatment is documented in Single Values as Lists.For example, if
var.pages
is null then the outermost block above effectively hasfor_each = []
, while ifvar.pages
is not-null thenfor_each
is a single-element list containing the same object thatvar.pages
contained, and so this effectively declares zero or onepages
blocks. The same is true for the nesteddynamic "source"
block.Bonus chatter: This
[*]
behavior of turning a single value into a zero-or-one-element tuple has an inverse in the form of theone
function.Although that's not directly useful in this case, these two features often appear together in a module if something else in the module later needs to refer to something that was previously turned into a tuple using
[*]
, as shown in Relationship to the "Splat" Operator.Therefore I think it's worth learning about them both, since together they allow you to move freely back and forth between single values that might be null and sequences of zero or one elements as you use other Terraform language features that expect the opposite of whichever you are currently holding.
Zero-or-one element sequences have essentially the same expressive power as single values that might be null, but many Terraform language features prioritize working with sequences because they can generalize to support arbitrary numbers of elements.