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