Xenoform is a preprocessor for terraform (or OpenTofu). By introducing some syntax extensions (explained below), xenoform provides better developer experience for your terraform projects.
Xenoform syntax is a superset of the original terraform syntax (to be precise, HCL2 syntax); input is converted into normal terraform code. Thus it's fairly simple to introduce xenoform into your terraform workflow.
See also tests/success/all_features.in.tf for a usage example.
-
Motivation
- Terraform
localsblock introduces module-local variables. Sometimes we want file-local or block-local variables.
- Terraform
-
Example:
flocals { # define file locals x = 1 } resource "someprovider_someresource" "xxx" { for_each = toset(["aaa", "bbb"]) blocals { # define variables that reside only in the current resource/module block foo = "xxx_${each.value}" } arg1 = flocal.x # expanded to `local.flocal_filename_x` arg2 = blocal.foo # expanded to `"xxx_${each.value}"` }
-
Note:
flocalsare implemented as simple name mangling:flocalsblock is converted tolocalsblock with a file-specific variable name prefix.blocalsare implemented by term rewriting.blocal.var_nameis replaced with the right hand side of theblocalsvariable. Therefore, in aresource/modulewithfor_each,eachcan be used in the right hand side ofblocalsvariables.
-
Motivation
- Terraform does not support custom functions in the HCL2 source files.
- Provider-defined functions is possible but it's somewhat tedious to define small utilities in Go.
- Pure-HCL2 way to reuse an expression is to define a terraform module, but it's syntactically heavy on the calling side.
- Terraform does not support custom functions in the HCL2 source files.
-
Example
macro "hoge" "x" "y" { # define a macro named `hoge` that receives 2 arguments x2 = x + 1 y2 = y + 2 return = x2 * y2 # the last block attribute must be `return` } locals { use_hoge = macro::hoge(5, 10) # expanded to `((5 + 1) * (10 + 2))` }
-
Include from code: By adding the following you can use macros defined in
another_file.in.tf.macro_include { source = "./another_file.in.tf" }
-
Include from command line: You can instead pass
--macro-prelude another_file.in.tfin order to use macros defined in the file.
- Xenoform CLI comes with a library of utility macros.
They are automatically included and are available in all xenoform code.
See macro_lib.in.tf
and the comments on
macroblocks for the details of available macros.
-
Motivation
- Large complex expressions such as nested for-expressions are hard to read. We can somewhat improve the readability by introducing locals for each step of the complex expression. But the locals could then be a source of confusions.
- We want to construct complex expressions from multiple steps as "lambda" expressions and pour the result of one step into the next step.
-
Example:
locals { # Expanded to `join(", ", [for x in concat([1, 2, 3], [4, 5]) : x + 1])` with some extra parens/spaces use_pipeline = macro::pipeline( [1, 2, 3], # 1st step (just an expression). concat(_, [4, 5]), # 2nd step (expression with a placeholder). `_` is replaced by the result of the 1st step. [for x in _ : x + 1], # 3rd step (expression with a placeholder). `_` is replaced by the result of the 2nd step. join(", ", _), # Last step (expression with a placeholder). `_` is replaced by the result of the 3rd step. ) }
-
Note:
pipelinemacro is a special macro implemented in the preprocessor, i.e., it's not a macro defined usingmacroblock. It takes a variable number of arguments (at least 1).
-
Motivation
- To enable code reuse with macros that look like higher-order functions.
- Sometimes you need to write terraform expressions that are almost the same but slightly different in a small part of the expression. To facilitate code reuse, it'd be nice to "inject" the different part of the code from the macro calling side.
- To enable code reuse with macros that look like higher-order functions.
-
Example:
locals { # Expanded to `10 + 20` with some extra parens/spaces use_bind = macro::bind(_1 + _2, 10, 20) }
First argument passed to
macro::bind()is an expression containing placeholder variables such as_1and_2.macro::bind()binds the remaining arguments to the placeholder variables. In this example,10is bound to_1, and20to_2.macro::bind()is not expected to be called in normal terraform code; it's useful when you are writing reusable macros.macro::bind()can be used as an invocation of the function-like expression injected from the caller side.macro "mymap" "arr" "f" { return = [for e in arr : macro::bind(f, e)] } locals { # Expanded to `[for e in [1, 2, 3] : e * 2]` with some extra parens/spaces use_mymap = macro::mymap([1, 2, 3], _1 * 2) }
-
Note:
bindmacro is a special macro implemented in the preprocessor, i.e., it's not a macro defined usingmacroblock. It takes a variable number of arguments (at least 1).
-
Motivation
- Terraform does not directly support
assertfunctionality in other languages. Validating if an expected precondition is met before executing plan/apply is useful for catching issues earlier. - Terraform does offer multiple ways to validate configurations, but there's currently no support for assertions that (a) does not belong to a specific resource, data source or module, and (b) raises an error before making any action.
assertblock is introduced to cover such needs.
- Terraform does not directly support
-
Example:
assert "string_not_equal" { condition = "an expression that should" == "evaluate to true but actually false" }
is expanded to a local and causes a plan-time error.
-
Note:
- An
assertblock is expanded to alocalswith a conditional (ternary) expression that raises an error only when the condition evaluates tofalse. To raise an error we usetobool()function (as a workaround), but the type conversion itself is not relevant; look into the string passed totoobool().
- An
- Some of code editor features (e.g. language server protocol) for terraform won't work.
- Xenoform outputs may contain extra parenthesis, whitespace and newline characters.
Xenoform itself does not try to prettify the output; when you want to remove extra
whitespaces use
terraform fmt. It also means that line numbers are not preserved. Be careful when you see line numbers in error/warning messages by terraform or other tools. - Macro bodies are expanded at call site. If a macro body contains variable references
such as
local.fooandflocal.barand the macro resides in another directory/file, the variables cannot be resolved from the call site.- Note: You can define a 0-arg macro as a preprocess-time constant.
- Code comments are basically preserved so that static code analysis tools can use them
(e.g. to suppress warnings of a tool). However, comments before xenoform-specific blocks
(
macro,blocalsandassert) are removed together with the blocks.
Main developer : @skirino