github.com/hashicorp/hcl/v2@v2.20.0/guide/go_patterns.rst (about) 1 Design Patterns for Complex Systems 2 =================================== 3 4 In previous sections we've seen an overview of some different ways an 5 application can decode a language its has defined in terms of the HCL grammar. 6 For many applications, those mechanisms are sufficient. However, there are 7 some more complex situations that can benefit from some additional techniques. 8 This section lists a few of these situations and ways to use the HCL API to 9 accommodate them. 10 11 .. _go-interdep-blocks: 12 13 Interdependent Blocks 14 --------------------- 15 16 In some configuration languages, the variables available for use in one 17 configuration block depend on values defined in other blocks. 18 19 For example, in Terraform many of the top-level constructs are also implicitly 20 definitions of values that are available for use in expressions elsewhere: 21 22 .. code-block:: hcl 23 24 variable "network_numbers" { 25 type = list(number) 26 } 27 28 variable "base_network_addr" { 29 type = string 30 default = "10.0.0.0/8" 31 } 32 33 locals { 34 network_blocks = { 35 for x in var.number: 36 x => cidrsubnet(var.base_network_addr, 8, x) 37 } 38 } 39 40 resource "cloud_subnet" "example" { 41 for_each = local.network_blocks 42 43 cidr_block = each.value 44 } 45 46 output "subnet_ids" { 47 value = cloud_subnet.example[*].id 48 } 49 50 In this example, the ``variable "network_numbers"`` block makes 51 ``var.network_numbers`` available to expressions, the 52 ``resource "cloud_subnet" "example"`` block makes ``cloud_subnet.example`` 53 available, etc. 54 55 Terraform achieves this by decoding the top-level structure in isolation to 56 start. You can do this either using the low-level API or using :go:pkg:`gohcl` 57 with :go:type:`hcl.Body` fields tagged as "remain". 58 59 Once you have a separate body for each top-level block, you can inspect each 60 of the attribute expressions inside using the ``Variables`` method on 61 :go:type:`hcl.Expression`, or the ``Variables`` function from package 62 :go:pkg:`hcldec` if you will eventually use its higher-level API to decode as 63 Terraform does. 64 65 The detected variable references can then be used to construct a dependency 66 graph between the blocks, and then perform a 67 `topological sort <https://en.wikipedia.org/wiki/Topological_sorting>`_ to 68 determine the correct order to evaluate each block's contents so that values 69 will always be available before they are needed. 70 71 Since :go:pkg:`cty` values are immutable, it is not convenient to directly 72 change values in a :go:type:`hcl.EvalContext` during this gradual evaluation, 73 so instead construct a specialized data structure that has a separate value 74 per object and construct an evaluation context from that each time a new 75 value becomes available. 76 77 Using :go:pkg:`hcldec` to evaluate block bodies is particularly convenient in 78 this scenario because it produces :go:type:`cty.Value` results which can then 79 just be directly incorporated into the evaluation context. 80 81 Distributed Systems 82 ------------------- 83 84 Distributed systems cause a number of extra challenges, and configuration 85 management is rarely the worst of these. However, there are some specific 86 considerations for using HCL-based configuration in distributed systems. 87 88 For the sake of this section, we are concerned with distributed systems where 89 at least two separate components both depend on the content of HCL-based 90 configuration files. Real-world examples include the following: 91 92 * **HashiCorp Nomad** loads configuration (job specifications) in its servers 93 but also needs these results in its clients and in its various driver plugins. 94 95 * **HashiCorp Terraform** parses configuration in Terraform Core but can write 96 a partially-evaluated execution plan to disk and continue evaluation in a 97 separate process later. It must also pass configuration values into provider 98 plugins. 99 100 Broadly speaking, there are two approaches to allowing configuration to be 101 accessed in multiple subsystems, which the following subsections will discuss 102 separately. 103 104 Ahead-of-time Evaluation 105 ^^^^^^^^^^^^^^^^^^^^^^^^ 106 107 Ahead-of-time evaluation is the simplest path, with the configuration files 108 being entirely evaluated on entry to the system, and then only the resulting 109 *constant values* being passed between subsystems. 110 111 This approach is relatively straightforward because the resulting 112 :go:type:`cty.Value` results can be losslessly serialized as either JSON or 113 msgpack as long as all system components agree on the expected value types. 114 Aside from passing these values around "on the wire", parsing and decoding of 115 configuration proceeds as normal. 116 117 Both Nomad and Terraform use this approach for interacting with *plugins*, 118 because the plugins themselves are written by various different teams that do 119 not coordinate closely, and so doing all expression evaluation in the core 120 subsystems ensures consistency between plugins and simplifies plugin development. 121 122 In both applications, the plugin is expected to describe (using an 123 application-specific protocol) the schema it expects for each element of 124 configuration it is responsible for, allowing the core subsystems to perform 125 decoding on the plugin's behalf and pass a value that is guaranteed to conform 126 to the schema. 127 128 Gradual Evaluation 129 ^^^^^^^^^^^^^^^^^^ 130 131 Although ahead-of-time evaluation is relatively straightforward, it has the 132 significant disadvantage that all data available for access via variables or 133 functions must be known by whichever subsystem performs that initial 134 evaluation. 135 136 For example, in Terraform, the "plan" subcommand is responsible for evaluating 137 the configuration and presenting to the user an execution plan for approval, but 138 certain values in that plan cannot be determined until the plan is already 139 being applied, since the specific values used depend on remote API decisions 140 such as the allocation of opaque id strings for objects. 141 142 In Terraform's case, both the creation of the plan and the eventual apply 143 of that plan *both* entail evaluating configuration, with the apply step 144 having a more complete set of input values and thus producing a more complete 145 result. However, this means that Terraform must somehow make the expressions 146 from the original input configuration available to the separate process that 147 applies the generated plan. 148 149 Good usability requires error and warning messages that are able to refer back 150 to specific sections of the input configuration as context for the reported 151 problem, and the best way to achieve this in a distributed system doing 152 gradual evaluation is to send the configuration *source code* between 153 subsystems. This is generally the most compact representation that retains 154 source location information, and will avoid any inconsistency caused by 155 introducing another intermediate serialization. 156 157 In Terraform's, for example, the serialized plan incorporates both the data 158 structure describing the partial evaluation results from the plan phase and 159 the original configuration files that produced those results, which can then 160 be re-evalauated during the apply step. 161 162 In a gradual evaluation scenario, the application should verify correctness of 163 the input configuration as completely as possible at each state. To help with 164 this, :go:pkg:`cty` has the concept of 165 `unknown values <https://github.com/zclconf/go-cty/blob/master/docs/concepts.md#unknown-values-and-the-dynamic-pseudo-type>`_, 166 which can stand in for values the application does not yet know while still 167 retaining correct type information. HCL expression evaluation reacts to unknown 168 values by performing type checking but then returning another unknown value, 169 causing the unknowns to propagate through expressions automatically. 170 171 .. code-block:: go 172 173 ctx := &hcl.EvalContext{ 174 Variables: map[string]cty.Value{ 175 "name": cty.UnknownVal(cty.String), 176 "age": cty.UnknownVal(cty.Number), 177 }, 178 } 179 val, moreDiags := expr.Value(ctx) 180 diags = append(diags, moreDiags...) 181 182 Each time an expression is re-evaluated with additional information, fewer of 183 the input values will be unknown and thus more of the result will be known. 184 Eventually the application should evaluate the expressions with no unknown 185 values at all, which then guarantees that the result will also be wholly-known. 186 187 Static References, Calls, Lists, and Maps 188 ----------------------------------------- 189 190 In most cases, we care more about the final result value of an expression than 191 how that value was obtained. A particular list argument, for example, might 192 be defined by the user via a tuple constructor, by a `for` expression, or by 193 assigning the value of a variable that has a suitable list type. 194 195 In some special cases, the structure of the expression is more important than 196 the result value, or an expression may not *have* a reasonable result value. 197 For example, in Terraform there are a few arguments that call for the user 198 to name another object by reference, rather than provide an object value: 199 200 .. code-block:: hcl 201 202 resource "cloud_network" "example" { 203 # ... 204 } 205 206 resource "cloud_subnet" "example" { 207 cidr_block = "10.1.2.0/24" 208 209 depends_on = [ 210 cloud_network.example, 211 ] 212 } 213 214 The ``depends_on`` argument in the second ``resource`` block *appears* as an 215 expression that would construct a single-element tuple containing an object 216 representation of the first resource block. However, Terraform uses this 217 expression to construct its dependency graph, and so it needs to see 218 specifically that this expression refers to ``cloud_network.example``, rather 219 than determine a result value for it. 220 221 HCL offers a number of "static analysis" functions to help with this sort of 222 situation. These all live in the :go:pkg:`hcl` package, and each one imposes 223 a particular requirement on the syntax tree of the expression it is given, 224 and returns a result derived from that if the expression conforms to that 225 requirement. 226 227 .. go:currentpackage:: hcl 228 229 .. go:function:: func ExprAsKeyword(expr Expression) string 230 231 This function attempts to interpret the given expression as a single keyword, 232 returning that keyword as a string if possible. 233 234 A "keyword" for the purposes of this function is an expression that can be 235 understood as a valid single identifier. For example, the simple variable 236 reference ``foo`` can be interpreted as a keyword, while ``foo.bar`` 237 cannot. 238 239 As a special case, the language-level keywords ``true``, ``false``, and 240 ``null`` are also considered to be valid keywords, allowing the calling 241 application to disregard their usual meaning. 242 243 If the given expression cannot be reduced to a single keyword, the result 244 is an empty string. Since an empty string is never a valid keyword, this 245 result unambiguously signals failure. 246 247 .. go:function:: func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) 248 249 This is a generalization of ``ExprAsKeyword`` that will accept anything that 250 can be interpreted as a *traversal*, which is a variable name followed by 251 zero or more attribute access or index operators with constant operands. 252 253 For example, all of ``foo``, ``foo.bar`` and ``foo[0]`` are valid 254 traversals, but ``foo[bar]`` is not, because the ``bar`` index is not 255 constant. 256 257 This is the function that Terraform uses to interpret the items within the 258 ``depends_on`` sequence in our example above. 259 260 As with ``ExprAsKeyword``, this function has a special case that the 261 keywords ``true``, ``false``, and ``null`` will be accepted as if they were 262 variable names by this function, allowing ``null.foo`` to be interpreted 263 as a traversal even though it would be invalid if evaluated. 264 265 If error diagnostics are returned, the traversal result is invalid and 266 should not be used. 267 268 .. go:function:: func RelTraversalForExpr(expr Expression) (Traversal, Diagnostics) 269 270 This is very similar to ``AbsTraversalForExpr``, but the result is a 271 *relative* traversal, which is one whose first name is considered to be 272 an attribute of some other (implied) object. 273 274 The processing rules are identical to ``AbsTraversalForExpr``, with the 275 only exception being that the first element of the returned traversal is 276 marked as being an attribute, rather than as a root variable. 277 278 .. go:function:: func ExprList(expr Expression) ([]Expression, Diagnostics) 279 280 This function requires that the given expression be a tuple constructor, 281 and if so returns a slice of the element expressions in that constructor. 282 Applications can then perform further static analysis on these, or evaluate 283 them as normal. 284 285 If error diagnostics are returned, the result is invalid and should not be 286 used. 287 288 This is the fucntion that Terraform uses to interpret the expression 289 assigned to ``depends_on`` in our example above, then in turn using 290 ``AbsTraversalForExpr`` on each enclosed expression. 291 292 .. go:function:: func ExprMap(expr Expression) ([]KeyValuePair, Diagnostics) 293 294 This function requires that the given expression be an object constructor, 295 and if so returns a slice of the element key/value pairs in that constructor. 296 Applications can then perform further static analysis on these, or evaluate 297 them as normal. 298 299 If error diagnostics are returned, the result is invalid and should not be 300 used. 301 302 .. go:function:: func ExprCall(expr Expression) (*StaticCall, Diagnostics) 303 304 This function requires that the given expression be a function call, and 305 if so returns an object describing the name of the called function and 306 expression objects representing the call arguments. 307 308 If error diagnostics are returned, the result is invalid and should not be 309 used. 310 311 The ``Variables`` method on :go:type:`hcl.Expression` is also considered to be 312 a "static analysis" helper, but is built in as a fundamental feature because 313 analysis of referenced variables is often important for static validation and 314 for implementing interdependent blocks as we saw in the section above. 315