guide: Guide-type documentation for HCL
This commit is contained in:
commit
e78fa0ded2
2
guide/.gitignore
vendored
Normal file
2
guide/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
env/*
|
||||
_build/*
|
20
guide/Makefile
Normal file
20
guide/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = HCL
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
157
guide/conf.py
Normal file
157
guide/conf.py
Normal file
@ -0,0 +1,157 @@
|
||||
import subprocess
|
||||
import os
|
||||
import os.path
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'HCL'
|
||||
copyright = u'2018, HashiCorp'
|
||||
author = u'HashiCorp'
|
||||
|
||||
if 'READTHEDOCS_VERSION' in os.environ:
|
||||
version_str = os.environ['READTHEDOCS_VERSION']
|
||||
else:
|
||||
version_str = subprocess.check_output(['git', 'describe', '--always']).strip()
|
||||
|
||||
# The short X.Y version
|
||||
version = unicode(version_str)
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = unicode(version_str)
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.githubpages',
|
||||
'sphinxcontrib.golangdomain',
|
||||
'sphinx.ext.autodoc',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path .
|
||||
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store', 'env']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'HCLdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'HCL.tex', u'HCL Documentation',
|
||||
u'HashiCorp', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'hcl', u'HCL Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'HCL', u'HCL Documentation',
|
||||
author, 'HCL', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
31
guide/go.rst
Normal file
31
guide/go.rst
Normal file
@ -0,0 +1,31 @@
|
||||
Using HCL in a Go application
|
||||
=============================
|
||||
|
||||
HCL is itself written in Go_ and currently it is primarily intended for use as
|
||||
a library within other Go programs.
|
||||
|
||||
This section describes a number of different ways HCL can be used to define
|
||||
and process a configuration language within a Go program. For simple situations,
|
||||
HCL can decode directly into Go ``struct`` values in a similar way as encoding
|
||||
packages such as ``encoding/json`` and ``encoding/xml``.
|
||||
|
||||
The HCL Go API also offers some alternative approaches however, for processing
|
||||
languages that may be more complex or that include portions whose expected
|
||||
structure cannot be determined until runtime.
|
||||
|
||||
The following sections give an overview of different ways HCL can be used in
|
||||
a Go program.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Sub-sections:
|
||||
|
||||
go_parsing
|
||||
go_diagnostics
|
||||
go_decoding_gohcl
|
||||
go_decoding_hcldec
|
||||
go_expression_eval
|
||||
go_decoding_lowlevel
|
||||
go_patterns
|
||||
|
||||
.. _Go: https://golang.org/
|
130
guide/go_decoding_gohcl.rst
Normal file
130
guide/go_decoding_gohcl.rst
Normal file
@ -0,0 +1,130 @@
|
||||
.. go:package:: gohcl
|
||||
|
||||
.. _go-decoding-gohcl:
|
||||
|
||||
Decoding Into Native Go Values
|
||||
==============================
|
||||
|
||||
The most straightforward way to access the content of an HCL file is to
|
||||
decode into native Go values using ``reflect``, similar to the technique used
|
||||
by packages like ``encoding/json`` and ``encoding/xml``.
|
||||
|
||||
Package ``gohcl`` provides functions for this sort of decoding. Function
|
||||
``DecodeBody`` attempts to extract values from an HCL *body* and write them
|
||||
into a Go value given as a pointer:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type ServiceConfig struct {
|
||||
Type string `hcl:"type,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
ListenAddr string `hcl:"listen_addr"`
|
||||
}
|
||||
type Config struct {
|
||||
IOMode string `hcl:"io_mode"`
|
||||
Services []ServiceConfig `hcl:"service,block"`
|
||||
}
|
||||
|
||||
var c Config
|
||||
moreDiags := gohcl.DecodeBody(f.Body, nil, &c)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
The above example decodes the *root body* of a file ``f``, presumably loaded
|
||||
previously using a parser, into the variable ``c``. The field labels within
|
||||
the struct types imply the schema of the expected language, which is a cut-down
|
||||
version of the hypothetical language we showed in :ref:`intro`.
|
||||
|
||||
The struct field labels consist of two comma-separated values. The first is
|
||||
the name of the corresponding argument or block type as it will appear in
|
||||
the input file, and the second is the type of element being named. If the
|
||||
second value is omitted, it defaults to ``attr``, requesting an attribute.
|
||||
|
||||
Nested blocks are represented by a struct or a slice of that struct, and the
|
||||
special element type ``label`` within that struct declares that each instance
|
||||
of that block type must be followed by one or more block labels. In the above
|
||||
example, the ``service`` block type is defined to require two labels, named
|
||||
``type`` and ``name``. For label fields in particular, the given name is used
|
||||
only to refer to the particular label in error messages when the wrong number
|
||||
of labels is used.
|
||||
|
||||
By default, all declared attributes and blocks are considered to be required.
|
||||
An optional value is indicated by making its field have a pointer type, in
|
||||
which case ``nil`` is written to indicate the absense of the argument.
|
||||
|
||||
The sections below discuss some additional decoding use-cases. For full details
|
||||
on the `gohcl` package, see
|
||||
`the godoc reference <https://godoc.org/github.com/hashicorp/hcl2/gohcl>`_.
|
||||
|
||||
.. _go-decoding-gohcl-evalcontext:
|
||||
|
||||
Variables and Functions
|
||||
-----------------------
|
||||
|
||||
By default, arguments given in the configuration may use only literal values
|
||||
and the built in expression language operators, such as arithmetic.
|
||||
|
||||
The second argument to ``gohcl.DecodeBody``, shown as ``nil`` in the previous
|
||||
example, allows the calling application to additionally offer variables and
|
||||
functions for use in expressions. Its value is a pointer to an
|
||||
``hcl.EvalContext``, which will be covered in more detail in the later section
|
||||
:ref:`go-expression-eval`. For now, a simple example of making the id of the
|
||||
current process available as a single variable called ``pid``:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type Context struct {
|
||||
Pid string
|
||||
}
|
||||
ctx := gohcl.EvalContext(&Context{
|
||||
Pid: os.Getpid()
|
||||
})
|
||||
var c Config
|
||||
moreDiags := gohcl.DecodeBody(f.Body, ctx, &c)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
``gohcl.EvalContext`` constructs an expression evaluation context from a Go
|
||||
struct value, making the fields available as variables and the methods
|
||||
available as functions, after transforming the field and method names such
|
||||
that each word (starting with an uppercase letter) is all lowercase and
|
||||
separated by underscores.
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
name = "example-program (${pid})"
|
||||
|
||||
Partial Decoding
|
||||
----------------
|
||||
|
||||
In the examples so far, we've extracted the content from the entire input file
|
||||
in a single call to ``DecodeBody``. This is sufficient for many simple
|
||||
situations, but sometimes different parts of the file must be evaluated
|
||||
separately. For example:
|
||||
|
||||
* If different parts of the file must be evaluated with different variables
|
||||
or functions available.
|
||||
|
||||
* If the result of evaluating one part of the file is used to set variables
|
||||
or functions in another part of the file.
|
||||
|
||||
There are several ways to perform partial decoding with ``gohcl``, all of
|
||||
which involve decoding into HCL's own types, such as ``hcl.Body``.
|
||||
|
||||
The most general approach is to declare an additional struct field of type
|
||||
``hcl.Body``, with the special field tag type ``remain``:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type ServiceConfig struct {
|
||||
Type string `hcl:"type,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
ListenAddr string `hcl:"listen_addr"`
|
||||
Remain hcl.Body `hcl:",remain"`
|
||||
}
|
||||
|
||||
When a ``remain`` field is present, any element of the input body that is
|
||||
not matched is retained in a body saved into that field, which can then be
|
||||
decoded in a later call, potentially with a different evaluation context.
|
||||
|
||||
Another option is to decode an attribute into a value of type `hcl.Expression`,
|
||||
which can then be evaluated separately as described in
|
||||
:ref:`expression-eval`.
|
242
guide/go_decoding_hcldec.rst
Normal file
242
guide/go_decoding_hcldec.rst
Normal file
@ -0,0 +1,242 @@
|
||||
.. go:package:: hcldec
|
||||
|
||||
.. _go-decoding-hcldec:
|
||||
|
||||
Decoding With Dynamic Schema
|
||||
============================
|
||||
|
||||
In section :ref:`go-decoding-gohcl`, we saw the most straightforward way to
|
||||
access the content from an HCL file, decoding directly into a Go value whose
|
||||
type is known at application compile time.
|
||||
|
||||
For some applications, it is not possible to know the schema of the entire
|
||||
configuration when the application is built. For example, `HashiCorp Terraform`_
|
||||
uses HCL as the foundation of its configuration language, but parts of the
|
||||
configuration are handled by plugins loaded dynamically at runtime, and so
|
||||
the schemas for these portions cannot be encoded directly in the Terraform
|
||||
source code.
|
||||
|
||||
HCL's ``hcldec`` package offers a different approach to decoding that allows
|
||||
schemas to be created at runtime, and the result to be decoded into
|
||||
dynamically-typed data structures.
|
||||
|
||||
The sections below are an overview of the main parts of package ``hcldec``.
|
||||
For full details, see
|
||||
`the package godoc <https://godoc.org/github.com/hashicorp/hcl2/hcldec>`_.
|
||||
|
||||
.. _`HashiCorp Terraform`: https://www.terraform.io/
|
||||
|
||||
Decoder Specification
|
||||
---------------------
|
||||
|
||||
Whereas :go:pkg:`gohcl` infers the expected schema by using reflection against
|
||||
the given value, ``hcldec`` obtains schema through a decoding *specification*,
|
||||
which is a set of instructions for mapping HCL constructs onto a dynamic
|
||||
data structure.
|
||||
|
||||
The ``hcldec`` package contains a number of different specifications, each
|
||||
implementing :go:type:`hcldec.Spec` and having a ``Spec`` suffix on its name.
|
||||
Each spec has two distinct functions:
|
||||
|
||||
* Adding zero or more validation constraints on the input configuration file.
|
||||
|
||||
* Producing a result value based on some elements from the input file.
|
||||
|
||||
The most common pattern is for the top-level spec to be a
|
||||
:go:type:`hcldec.ObjectSpec` with nested specifications defining either blocks
|
||||
or attributes, depending on whether the configuration file will be
|
||||
block-structured or flat.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
spec := hcldec.ObjectSpec{
|
||||
"io_mode": &hcldec.AttrSpec{
|
||||
Name: "io_mode",
|
||||
Type: cty.String,
|
||||
},
|
||||
"services": &hcldec.BlockMapSpec{
|
||||
TypeName: "service",
|
||||
LabelNames: []string{"type", "name"},
|
||||
Nested: hcldec.ObjectSpec{
|
||||
"listen_addr": &hcldec.AttrSpec{
|
||||
Name: "listen_addr",
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
"processes": &hcldec.BlockMapSpec{
|
||||
TypeName: "service",
|
||||
LabelNames: []string{"name"},
|
||||
Nested: hcldec.ObjectSpec{
|
||||
"command": &hcldec.AttrSpec{
|
||||
Name: "command",
|
||||
Type: cty.List(cty.String),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
val, moreDiags := hcldec.Decode(f.Body, spec, nil)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
The above specification expects a configuration shaped like our example in
|
||||
:ref:`intro`, and calls for it to be decoded into a dynamic data structure
|
||||
that would have the following shape if serialized to JSON:
|
||||
|
||||
.. code-block:: JSON
|
||||
|
||||
{
|
||||
"io_mode": "async",
|
||||
"services": {
|
||||
"http": {
|
||||
"web_proxy": {
|
||||
"listen_addr": "127.0.0.1:8080",
|
||||
"processes": {
|
||||
"main": {
|
||||
"command": ["/usr/local/bin/awesome-app", "server"]
|
||||
},
|
||||
"mgmt": {
|
||||
"command": ["/usr/local/bin/awesome-app", "mgmt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. go:package:: cty
|
||||
|
||||
Types and Values With ``cty``
|
||||
-----------------------------
|
||||
|
||||
HCL's expression interpreter is implemented in terms of another library called
|
||||
:go:pkg:`cty`, which provides a type system which HCL builds on and a robust
|
||||
representation of dynamic values in that type system. You could think of
|
||||
:go:pkg:`cty` as being a bit like Go's own :go:pkg:`reflect`, but for the
|
||||
results of HCL expressions rather than Go programs.
|
||||
|
||||
The full details of this system can be found in
|
||||
`its own repository <https://github.com/zclconf/go-cty>`_, but this section
|
||||
will cover the most important highlights, because ``hcldec`` specifications
|
||||
include :go:pkg:`cty` types (as seen in the above example) and its results are
|
||||
:go:pkg:`cty` values.
|
||||
|
||||
``hcldec`` works directly with :go:pkg:`cty` — as opposed to converting values
|
||||
directly into Go native types — because the functionality of the :go:pkg:`cty`
|
||||
packages then allows further processing of those values without any loss of
|
||||
fidelity or range. For example, :go:pkg:`cty` defines a JSON encoding of its
|
||||
values that can be decoded losslessly as long as both sides agree on the value
|
||||
type that is expected, which is a useful capability in systems where some sort
|
||||
of RPC barrier separates the main program from its plugins.
|
||||
|
||||
Types are instances of :go:type:`cty.Type`, and are constructed from functions
|
||||
and variables in :go:pkg:`cty` as shown in the above example, where the string
|
||||
attributes are typed as ``cty.String``, which is a primitive type, and the list
|
||||
attribute is typed as ``cty.List(cty.String)``, which constructs a new list
|
||||
type with string elements.
|
||||
|
||||
Values are instances of :go:type:`cty.Value`, and can also be constructed from
|
||||
functions in :go:pkg:`cty`, using the functions that include ``Val`` in their
|
||||
names or using the operation methods available on :go:type:`cty.Value`.
|
||||
|
||||
In most cases you will eventually want to use the resulting data as native Go
|
||||
types, to pass it to non-:go:pkg:`cty`-aware code. To do this, see the guides
|
||||
on
|
||||
`Converting between types <https://github.com/zclconf/go-cty/blob/master/docs/convert.md>`_
|
||||
(staying within :go:pkg:`cty`) and
|
||||
`Converting to and from native Go values <https://github.com/zclconf/go-cty/blob/master/docs/gocty.md>`_.
|
||||
|
||||
Partial Decoding
|
||||
----------------
|
||||
|
||||
Because the ``hcldec`` result is always a value, the input is always entirely
|
||||
processed in a single call, unlike with :go:pkg:`gohcl`.
|
||||
|
||||
However, both :go:pkg:`gohcl` and :go:pkg:`hcldec` take :go:type:`hcl.Body` as
|
||||
the representation of input, and so it is possible and common to mix them both
|
||||
in the same program.
|
||||
|
||||
A common situation is that :go:pkg:`gohcl` is used in the main program to
|
||||
decode the top level of configuration, which then allows the main program to
|
||||
determine which plugins need to be loaded to process the leaf portions of
|
||||
configuration. In this case, the portions that will be interpreted by plugins
|
||||
are retained as opaque :go:type:`hcl.Body` until the plugins have been loaded,
|
||||
and then each plugin provides its :go:type:`hcldec.Spec` to allow decoding the
|
||||
plugin-specific configuration into a :go:type:`cty.Value` which be
|
||||
transmitted to the plugin for further processing.
|
||||
|
||||
In our example from :ref:`intro`, perhaps each of the different service types
|
||||
is managed by a plugin, and so the main program would decode the block headers
|
||||
to learn which plugins are needed, but process the block bodies dynamically:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
type ServiceConfig struct {
|
||||
Type string `hcl:"type,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
PluginConfig hcl.Body `hcl:",remain"`
|
||||
}
|
||||
type Config struct {
|
||||
IOMode string `hcl:"io_mode"`
|
||||
Services []ServiceConfig `hcl:"service,block"`
|
||||
}
|
||||
|
||||
var c Config
|
||||
moreDiags := gohcl.DecodeBody(f.Body, nil, &c)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
// (show diags in the UI)
|
||||
return
|
||||
}
|
||||
|
||||
for _, sc := range c.Services {
|
||||
pluginName := block.Type
|
||||
|
||||
// Totally-hypothetical plugin manager (not part of HCL)
|
||||
plugin, err := pluginMgr.GetPlugin(pluginName)
|
||||
if err != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{ /* ... */ })
|
||||
continue
|
||||
}
|
||||
spec := plugin.ConfigSpec() // returns hcldec.Spec
|
||||
|
||||
// Decode the block body using the plugin's given specification
|
||||
configVal, moreDiags := hcldec.Decode(sc.PluginConfig, spec, nil)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Again, hypothetical API within your application itself, and not
|
||||
// part of HCL. Perhaps plugin system serializes configVal as JSON
|
||||
// and sends it over to the plugin.
|
||||
svc := plugin.NewService(configVal)
|
||||
serviceMgr.AddService(sc.Name, svc)
|
||||
}
|
||||
|
||||
|
||||
Variables and Functions
|
||||
-----------------------
|
||||
|
||||
The final argument to ``hcldec.Decode`` is an expression evaluation context,
|
||||
just as with ``gohcl.DecodeBlock``.
|
||||
|
||||
This object can be constructed using
|
||||
:ref:`the gohcl helper function <go-decoding-gohcl-evalcontext>` as before if desired, but
|
||||
you can also choose to work directly with :go:type:`hcl.EvalContext` as
|
||||
discussed in :ref:`go-expression-eval`:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"pid": cty.NumberIntVal(int64(os.Getpid())),
|
||||
},
|
||||
}
|
||||
val, moreDiags := hcldec.Decode(f.Body, spec, ctx)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
As you can see, this lower-level API also uses :go:pkg:`cty`, so it can be
|
||||
particularly convenient in situations where the result of dynamically decoding
|
||||
one block must be available to expressions in another block.
|
199
guide/go_decoding_lowlevel.rst
Normal file
199
guide/go_decoding_lowlevel.rst
Normal file
@ -0,0 +1,199 @@
|
||||
.. _go-decoding-lowlevel:
|
||||
|
||||
Advanced Decoding With The Low-level API
|
||||
========================================
|
||||
|
||||
In previous sections we've discussed :go:pkg:`gohcl` and :go:pkg:`hcldec`,
|
||||
which both deal with decoding of HCL bodies and the expressions within them
|
||||
using a high-level description of the expected configuration schema.
|
||||
Both of these packages are implemented in terms of HCL's low-level decoding
|
||||
interfaces, which we will explore in this section.
|
||||
|
||||
HCL decoding in the low-level API has two distinct phases:
|
||||
|
||||
* Structural decoding: analyzing the arguments and nested blocks present in a
|
||||
particular body.
|
||||
|
||||
* Expression evaluation: obtaining final values for each argument expression
|
||||
found during structural decoding.
|
||||
|
||||
The low-level API gives the calling application full control over when each
|
||||
body is decoded and when each expression is evaluated, allowing for more
|
||||
complex configuration formats where e.g. different variables are available in
|
||||
different contexts, or perhaps expressions within one block can refer to
|
||||
values defined in another block.
|
||||
|
||||
The low-level API also gives more detailed access to source location
|
||||
information for decoded elements, and so may be desirable for applications that
|
||||
do a lot of additional validation of decoded data where more specific source
|
||||
locations lead to better diagnostic messages.
|
||||
|
||||
Since all of the decoding mechanisms work with the same :go:type:`hcl.Body`
|
||||
type, it is fine and expected to mix them within an application to get access
|
||||
to the more detailed information where needed while using the higher-level APIs
|
||||
for the more straightforward portions of a configuration language.
|
||||
|
||||
The following subsections will give an overview of the low-level API. For full
|
||||
details, see `the godoc reference <https://godoc.org/github.com/hashicorp/hcl2/hcl>`_.
|
||||
|
||||
Structural Decoding
|
||||
-------------------
|
||||
|
||||
As seen in prior sections, :go:type:`hcl.Body` is an opaque representation of
|
||||
the arguments and child blocks at a particular nesting level. An HCL file has
|
||||
a root body containing the top-level elements, and then each nested block has
|
||||
its own body presenting its own content.
|
||||
|
||||
:go:type:`hcl.Body` is a Go interface whose methods serve as the structural
|
||||
decoding API:
|
||||
|
||||
.. go:currentpackage:: hcl
|
||||
|
||||
.. go:type:: Body
|
||||
|
||||
Represents the structural elements at a particular nesting level.
|
||||
|
||||
.. go:function:: func (b Body) Content(schema *BodySchema) (*BodyContent, Diagnostics)
|
||||
|
||||
Decode the content from the receiving body using the given schema. The
|
||||
schema is considered exhaustive of all content within the body, and so
|
||||
any elements not covered by the schema will generate error diagnostics.
|
||||
|
||||
.. go:function:: func (b Body) PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics)
|
||||
|
||||
Similar to `Content`, but allows for additional arguments and block types
|
||||
that are not described in the given schema. The additional body return
|
||||
value is a special body that contains only the *remaining* elements, after
|
||||
extraction of the ones covered by the schema. This returned body can be
|
||||
used to decode the remaining content elsewhere in the calling program.
|
||||
|
||||
.. go:function:: func (b Body) JustAttributes() (Attributes, Diagnostics)
|
||||
|
||||
Decode the content from the receving body in a special *attributes-only*
|
||||
mode, allowing the calling application to enumerate the arguments given
|
||||
inside the body without needing to predict them in schema.
|
||||
|
||||
When this method is used, a body can be treated somewhat like a map
|
||||
expression, but it still has a rigid structure where the arguments must
|
||||
be given directly with no expression evaluation. This is an advantage for
|
||||
declarations that must themselves be resolved before expression
|
||||
evaluation is possible.
|
||||
|
||||
If the body contains any blocks, error diagnostics are returned. JSON
|
||||
syntax relies on schema to distinguish arguments from nested blocks, and
|
||||
so a JSON body in attributes-only mode will treat all JSON object
|
||||
properties as arguments.
|
||||
|
||||
.. go:function:: func (b Body) MissingItemRange() Range
|
||||
|
||||
Returns a source range that points to where an absent required item in
|
||||
the body might be placed. This is a "best effort" sort of thing, required
|
||||
only to be somewhere inside the receving body, as a way to give source
|
||||
location information for a "missing required argument" sort of error.
|
||||
|
||||
The main content-decoding methods each require a :go:type:`hcl.BodySchema`
|
||||
object describing the expected content. The fields of this type describe the
|
||||
expected arguments and nested block types respectively:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
schema := &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "io_mode",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "service",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
content, moreDiags := body.Content(schema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
:go:type:`hcl.BodyContent` is the result of both ``Content`` and
|
||||
``PartialContent``, giving the actual attributes and nested blocks that were
|
||||
found. Since arguments are uniquely named within a body and unordered, they
|
||||
are returned as a map. Nested blocks are ordered and may have many instances
|
||||
of a given type, so they are returned all together in a single slice for
|
||||
further interpretation by the caller.
|
||||
|
||||
Unlike the two higher-level approaches, the low-level API *always* works only
|
||||
with one nesting level at a time. Decoding a nested block returns the "header"
|
||||
for that block, giving its type and label values, but its body remains an
|
||||
:go:type:`hcl.Body` for later decoding.
|
||||
|
||||
Each returned attribute corresponds to one of the arguments in the body, and
|
||||
it has an :go:type:`hcl.Expression` object that can be used to obtain a value
|
||||
for the argument during expression evaluation, as described in the next
|
||||
section.
|
||||
|
||||
Expression Evaluation
|
||||
---------------------
|
||||
|
||||
Expression evaluation *in general* has its own section, imaginitively titled
|
||||
:ref:`go-expression-eval`, so this section will focus only on how it is
|
||||
achieved in the low-level API.
|
||||
|
||||
All expression evaluation in the low-level API starts with an
|
||||
:go:type:`hcl.Expression` object. This is another interface type, with various
|
||||
implementations depending on the expression type and the syntax it was parsed
|
||||
from.
|
||||
|
||||
.. go:currentpackage:: hcl
|
||||
|
||||
.. go:type:: Expression
|
||||
|
||||
Represents a unevaluated single expression.
|
||||
|
||||
.. go:function:: func (e Expression) Value(ctx *EvalContext) (cty.Value, Diagnostics)
|
||||
|
||||
Evaluates the receiving expression in the given evaluation context. The
|
||||
result is a :go:type:`cty.Value` representing the result value, along
|
||||
with any diagnostics that were raised during evaluation.
|
||||
|
||||
If the diagnostics contains errors, the value may be incomplete or
|
||||
invalid and should either be discarded altogether or used with care for
|
||||
analysis.
|
||||
|
||||
.. go:function:: func (e Expression) Variables() []Traversal
|
||||
|
||||
Returns information about any nested expressions that access variables
|
||||
from the *global* evaluation context. Does not include references to
|
||||
temporary local variables, such as those generated by a
|
||||
"``for`` expression".
|
||||
|
||||
.. go:function:: func (e Expression) Range() Range
|
||||
|
||||
Returns the source range for the entire expression. This can be useful
|
||||
when generating application-specific diagnostic messages, such as
|
||||
value validation errors.
|
||||
|
||||
.. go:function:: func (e Expression) StartRange() Range
|
||||
|
||||
Similar to ``Range``, but if the expression is complex, such as a tuple
|
||||
or object constructor, may indicate only the opening tokens for the
|
||||
construct to avoid creating an overwhelming source code snippet.
|
||||
|
||||
This should be used in diagnostic messages only in situations where the
|
||||
error is clearly with the construct itself and not with the overall
|
||||
expression. For example, a type error indicating that a tuple was not
|
||||
expected might use ``StartRange`` to draw attention to the beginning
|
||||
of a tuple constructor, without highlighting the entire expression.
|
||||
|
||||
Method ``Value`` is the primary API for expressions, and takes the same kind
|
||||
of evaluation context object described in :ref:`go-expression-eval`.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"name": cty.StringVal("Ermintrude"),
|
||||
"age": cty.NumberIntVal(32),
|
||||
},
|
||||
}
|
||||
val, moreDiags := expr.Value(ctx)
|
||||
diags = append(diags, moreDiags...)
|
97
guide/go_diagnostics.rst
Normal file
97
guide/go_diagnostics.rst
Normal file
@ -0,0 +1,97 @@
|
||||
.. _go-diagnostics:
|
||||
|
||||
Diagnostic Messages
|
||||
===================
|
||||
|
||||
An important concern for any machine language intended for human authoring is
|
||||
to produce good error messages when the input is somehow invalid, or has
|
||||
other problems.
|
||||
|
||||
HCL uses *diagnostics* to describe problems in an end-user-oriented manner,
|
||||
such that the calling application can render helpful error or warning messages.
|
||||
The word "diagnostic" is a general term that covers both errors and warnings,
|
||||
where errors are problems that prevent complete processing while warnings are
|
||||
possible concerns that do not block processing.
|
||||
|
||||
HCL deviates from usual Go API practice by returning its own ``hcl.Diagnostics``
|
||||
type, instead of Go's own ``error`` type. This allows functions to return
|
||||
warnings without accompanying errors while not violating the usual expectation
|
||||
that the absense of errors is indicated by a nil ``error``.
|
||||
|
||||
In order to easily accumulate and return multiple diagnostics at once, the
|
||||
usual pattern for functions returning diagnostics is to gather them in a
|
||||
local variable and then return it at the end of the function, or possibly
|
||||
earlier if the function cannot continue due to the problems.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
func returningDiagnosticsExample() hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
// ...
|
||||
|
||||
// Call a function that may itself produce diagnostics.
|
||||
f, moreDiags := parser.LoadHCLFile("example.conf")
|
||||
// always append, in case warnings are present
|
||||
diags = append(diags, moreDiags...)
|
||||
if diags.HasErrors() {
|
||||
// If we can't safely continue in the presence of errors here, we
|
||||
// can optionally return early.
|
||||
return diags
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
A common variant of the above pattern is calling another diagnostics-generating
|
||||
function in a loop, using ``continue`` to begin the next iteration when errors
|
||||
are detected, but still completing all iterations and returning the union of
|
||||
all of the problems encountered along the way.
|
||||
|
||||
In :ref:`go-parsing`, we saw that the parser can generate diagnostics which
|
||||
are related to syntax problems within the loaded file. Further steps to decode
|
||||
content from the loaded file can also generate diagnostics related to *semantic*
|
||||
problems within the file, such as invalid expressions or type mismatches, and
|
||||
so a program using HCL will generally need to accumulate diagnostics across
|
||||
these various steps and then render them in the application UI somehow.
|
||||
|
||||
Rendering Diagnostics in the UI
|
||||
-------------------------------
|
||||
|
||||
The best way to render diagnostics to an end-user will depend a lot on the
|
||||
type of application: they might be printed into a terminal, written into a
|
||||
log for later review, or even shown in a GUI.
|
||||
|
||||
HCL leaves the responsibility for rendering diagnostics to the calling
|
||||
application, but since rendering to a terminal is a common case for command-line
|
||||
tools, the `hcl` package contains a default implementation of this in the
|
||||
form of a "diagnostic text writer":
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
wr := hcl.NewDiagnosticTextWriter(
|
||||
os.Stdout, // writer to send messages to
|
||||
parser.Files(), // the parser's file cache, for source snippets
|
||||
78, // wrapping width
|
||||
true, // generate colored/highlighted output
|
||||
)
|
||||
wr.WriteDiagnostics(diags)
|
||||
|
||||
This default implementation of diagnostic rendering includes relevant lines
|
||||
of source code for context, like this:
|
||||
|
||||
::
|
||||
|
||||
Error: Unsupported block type
|
||||
|
||||
on example.tf line 4, in resource "aws_instance" "example":
|
||||
2: provisionr "local-exec" {
|
||||
|
||||
Blocks of type "provisionr" are not expected here. Did you mean "provisioner"?
|
||||
|
||||
If the "color" flag is enabled, the severity will be additionally indicated by
|
||||
a text color and the relevant portion of the source code snippet will be
|
||||
underlined to draw further attention.
|
||||
|
149
guide/go_expression_eval.rst
Normal file
149
guide/go_expression_eval.rst
Normal file
@ -0,0 +1,149 @@
|
||||
.. _go-expression-eval:
|
||||
|
||||
Expression Evaluation
|
||||
=====================
|
||||
|
||||
Each argument attribute in a configuration file is interpreted as an
|
||||
expression. In the HCL native syntax, certain basic expression functionality
|
||||
is always available, such as arithmetic and template strings, and the calling
|
||||
application can extend this by making available specific variables and/or
|
||||
functions via an *evaluation context*.
|
||||
|
||||
We saw in :ref:`go-decoding-gohcl` and :ref:`go-decoding-hcldec` some basic
|
||||
examples of populating an evaluation context to make a variable available.
|
||||
This section will look more closely at the ``hcl.EvalContext`` type and how
|
||||
HCL expression evaluation behaves in different cases.
|
||||
|
||||
This section does not discuss in detail the expression syntax itself. For more
|
||||
information on that, see the HCL Native Syntax specification.
|
||||
|
||||
.. go:currentpackage:: hcl
|
||||
|
||||
.. go:type:: EvalContext
|
||||
|
||||
``hcl.EvalContext`` is the type used to describe the variables and functions
|
||||
available during expression evaluation, if any. Its usage is described in
|
||||
the following sections.
|
||||
|
||||
Defining Variables
|
||||
------------------
|
||||
|
||||
As we saw in :ref:`go-decoding-hcldec`, HCL represents values using an
|
||||
underlying library called :go:pkg:`cty`. When defining variables, their values
|
||||
must be given as :go:type:`cty.Value` values.
|
||||
|
||||
A full description of the types and value constructors in :go:pkg:`cty` is
|
||||
in `the reference documentation <https://github.com/zclconf/go-cty/blob/master/docs/types.md>`_.
|
||||
Variables in HCL are defined by assigning values into a map from string names
|
||||
to :go:type:`cty.Value`:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"name": cty.StringVal("Ermintrude"),
|
||||
"age": cty.NumberIntVal(32),
|
||||
},
|
||||
}
|
||||
|
||||
If this evaluation context were passed to one of the evaluation functions we
|
||||
saw in previous sections, the user would be able to refer to these variable
|
||||
names in any argument expression appearing in the evaluated portion of
|
||||
configuration:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
message = "${name} is ${age} ${age == 1 ? "year" : "years"} old!"
|
||||
|
||||
If you place ``cty``'s *object* values in the evaluation context, then their
|
||||
attributes can be referenced using the HCL attribute syntax, allowing for more
|
||||
complex structures:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"path": cty.ObjectVal(map[string]cty.Value{
|
||||
"root": cty.StringVal(rootDir),
|
||||
"module": cty.StringVal(moduleDir),
|
||||
"current": cty.StringVal(currentDir),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
source_file = "${path.module}/foo.txt"
|
||||
|
||||
.. _go-expression-funcs:
|
||||
|
||||
Defining Functions
|
||||
------------------
|
||||
|
||||
Custom functions can be defined by you application to allow users of its
|
||||
language to transform data in application-specific ways. The underlying
|
||||
function mechanism is also provided by :go:pkg:`cty`, allowing you to define
|
||||
the arguments a given function expects, what value type it will return for
|
||||
given argument types, etc. The full functions model is described in the
|
||||
``cty`` documentation section
|
||||
`Functions System <https://github.com/zclconf/go-cty/blob/master/docs/functions.md>`_.
|
||||
|
||||
There are `a number of "standard library" functions <https://godoc.org/github.com/apparentlymart/go-cty/cty/function/stdlib>`_
|
||||
available in a ``stdlib`` package within the :go:pkg:`cty` repository, avoiding
|
||||
the need for each application to re-implement basic functions for string
|
||||
manipulation, list manipulation, etc. It also includes function-shaped versions
|
||||
of several operations that are native operators in HCL, which should generally
|
||||
*not* be exposed as functions in HCL-based configurationf formats to avoid user
|
||||
confusion.
|
||||
|
||||
You can define functions in the ``Functions`` field of :go:type:`hcl.EvalContext`:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"name": cty.StringVal("Ermintrude"),
|
||||
},
|
||||
Functions: map[string]function.Function{
|
||||
"upper": stdlib.UpperFunc,
|
||||
"lower": stdlib.LowerFunc,
|
||||
"min": stdlib.MinFunc,
|
||||
"max": stdlib.MaxFunc,
|
||||
"strlen": stdlib.StrlenFunc,
|
||||
"substr": stdlib.SubstrFunc,
|
||||
},
|
||||
}
|
||||
|
||||
If this evaluation context were passed to one of the evaluation functions we
|
||||
saw in previous sections, the user would be able to call any of these functions
|
||||
in any argument expression appearing in the evaluated portion of configuration:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
message = "HELLO, ${upper(name)}!"
|
||||
|
||||
Expression Evaluation Modes
|
||||
---------------------------
|
||||
|
||||
HCL uses a different expression evaluation mode depending on the evaluation
|
||||
context provided. In HCL native syntax, evaluation modes are used to provide
|
||||
more relevant error messages. In JSON syntax, which embeds the native
|
||||
expression syntax in strings using "template" syntax, the evaluation mode
|
||||
determines whether strings are evaluated as templates at all.
|
||||
|
||||
If the given :go:type:`hcl.EvalContext` is ``nil``, native syntax expressions
|
||||
will react to users attempting to refer to variables or functions by producing
|
||||
errors indicating that these features are not available at all, rather than
|
||||
by saying that the specific variable or function does not exist. JSON syntax
|
||||
strings will not be evaluated as templates *at all* in this mode, making them
|
||||
function as literal strings.
|
||||
|
||||
If the evaluation context is non-``nil`` but either ``Variables`` or
|
||||
``Functions`` within it is ``nil``, native syntax will similarly produce
|
||||
"not supported" error messages. JSON syntax strings *will* parse templates
|
||||
in this case, but can also generate "not supported" messages if e.g. the
|
||||
user accesses a variable when the variables map is ``nil``.
|
||||
|
||||
If neither map is ``nil``, HCL assumes that both variables and functions are
|
||||
supported and will instead produce error messages stating that the specific
|
||||
variable or function accessed by the user is not defined.
|
64
guide/go_parsing.rst
Normal file
64
guide/go_parsing.rst
Normal file
@ -0,0 +1,64 @@
|
||||
.. _go-parsing:
|
||||
|
||||
Parsing HCL Input
|
||||
=================
|
||||
|
||||
The first step in processing HCL input provided by a user is to parse it.
|
||||
Parsing turns the raw bytes from an input file into a higher-level
|
||||
representation of the arguments and blocks, ready to be *decoded* into an
|
||||
application-specific form.
|
||||
|
||||
The main entry point into HCL parsing is :go:pkg:`hclparse`, which provides
|
||||
:go:type:`hclparse.Parser`:
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
parser := hclparse.NewParser()
|
||||
f, diags := parser.ParseHCLFile("server.conf")
|
||||
|
||||
Variable ``f`` is then a pointer to an :go:type:`hcl.File`, which is an
|
||||
opaque abstract representation of the file, ready to be decoded.
|
||||
|
||||
Variable ``diags`` describes any errors or warnings that were encountered
|
||||
during processing; HCL conventionally uses this in place of the usual ``error``
|
||||
return value in Go, to allow returning a mixture of multiple errors and
|
||||
warnings together with enough information to present good error messages to the
|
||||
user. We'll cover this in more detail in the next section,
|
||||
:ref:`go-diagnostics`.
|
||||
|
||||
.. go:package:: hclparse
|
||||
|
||||
Package ``hclparse``
|
||||
--------------------
|
||||
|
||||
.. go:type:: Parser
|
||||
|
||||
.. go:function:: func NewParser() *Parser
|
||||
|
||||
Constructs a new parser object. Each parser contains a cache of files
|
||||
that have already been read, so repeated calls to load the same file
|
||||
will return the same object.
|
||||
|
||||
.. go:function:: func (*Parser) ParseHCL(src []byte, filename string) (*hcl.File, hcl.Diagnostics)
|
||||
|
||||
Parse the given source code as HCL native syntax, saving the result into
|
||||
the parser's file cache under the given filename.
|
||||
|
||||
.. go:function:: func (*Parser) ParseHCLFile(filename string) (*hcl.File, hcl.Diagnostics)
|
||||
|
||||
Parse the contents of the given file as HCL native syntax. This is a
|
||||
convenience wrapper around ParseHCL that first reads the file into memory.
|
||||
|
||||
.. go:function:: func (*Parser) ParseJSON(src []byte, filename string) (*hcl.File, hcl.Diagnostics)
|
||||
|
||||
Parse the given source code as JSON syntax, saving the result into
|
||||
the parser's file cache under the given filename.
|
||||
|
||||
.. go:function:: func (*Parser) ParseJSONFile(filename string) (*hcl.File, hcl.Diagnostics)
|
||||
|
||||
Parse the contents of the given file as JSON syntax. This is a
|
||||
convenience wrapper around ParseJSON that first reads the file into memory.
|
||||
|
||||
The above list just highlights the main functions in this package.
|
||||
For full documentation, see
|
||||
`the hclparse godoc <https://godoc.org/github.com/hashicorp/hcl2/hclparse>`_.
|
315
guide/go_patterns.rst
Normal file
315
guide/go_patterns.rst
Normal file
@ -0,0 +1,315 @@
|
||||
Design Patterns for Complex Systems
|
||||
===================================
|
||||
|
||||
In previous sections we've seen an overview of some different ways an
|
||||
application can decode a language its has defined in terms of the HCL grammar.
|
||||
For many applications, those mechanisms are sufficient. However, there are
|
||||
some more complex situations that can benefit from some additional techniques.
|
||||
This section lists a few of these situations and ways to use the HCL API to
|
||||
accommodate them.
|
||||
|
||||
.. _go-interdep-blocks:
|
||||
|
||||
Interdependent Blocks
|
||||
---------------------
|
||||
|
||||
In some configuration languages, the variables available for use in one
|
||||
configuration block depend on values defined in other blocks.
|
||||
|
||||
For example, in Terraform many of the top-level constructs are also implicitly
|
||||
definitions of values that are available for use in expressions elsewhere:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
variable "network_numbers" {
|
||||
type = list(number)
|
||||
}
|
||||
|
||||
variable "base_network_addr" {
|
||||
type = string
|
||||
default = "10.0.0.0/8"
|
||||
}
|
||||
|
||||
locals {
|
||||
network_blocks = {
|
||||
for x in var.number:
|
||||
x => cidrsubnet(var.base_network_addr, 8, x)
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloud_subnet" "example" {
|
||||
for_each = local.network_blocks
|
||||
|
||||
cidr_block = each.value
|
||||
}
|
||||
|
||||
output "subnet_ids" {
|
||||
value = cloud_subnet.example[*].id
|
||||
}
|
||||
|
||||
In this example, the `variable "network_numbers"` block makes
|
||||
``var.base_network_addr`` available to expressions, the
|
||||
``resource "cloud_subnet" "example"`` block makes ``cloud_subnet.example``
|
||||
available, etc.
|
||||
|
||||
Terraform achieves this by decoding the top-level structure in isolation to
|
||||
start. You can do this either using the low-level API or using :go:pkg:`gohcl`
|
||||
with :go:type:`hcl.Body` fields tagged as "remain".
|
||||
|
||||
Once you have a separate body for each top-level block, you can inspect each
|
||||
of the attribute expressions inside using the ``Variables`` method on
|
||||
:go:type:`hcl.Expression`, or the ``Variables`` function from package
|
||||
:go:pkg:`hcldec` if you will eventually use its higher-level API to decode as
|
||||
Terraform does.
|
||||
|
||||
The detected variable references can then be used to construct a dependency
|
||||
graph between the blocks, and then perform a
|
||||
`topological sort <https://en.wikipedia.org/wiki/Topological_sorting>`_ to
|
||||
determine the correct order to evaluate each block's contents so that values
|
||||
will always be available before they are needed.
|
||||
|
||||
Since :go:pkg:`cty` values are immutable, it is not convenient to directly
|
||||
change values in a :go:type:`hcl.EvalContext` during this gradual evaluation,
|
||||
so instead construct a specialized data structure that has a separate value
|
||||
per object and construct an evaluation context from that each time a new
|
||||
value becomes available.
|
||||
|
||||
Using :go:pkg:`hcldec` to evaluate block bodies is particularly convenient in
|
||||
this scenario because it produces :go:type:`cty.Value` results which can then
|
||||
just be directly incorporated into the evaluation context.
|
||||
|
||||
Distributed Systems
|
||||
-------------------
|
||||
|
||||
Distributed systems cause a number of extra challenges, and configuration
|
||||
management is rarely the worst of these. However, there are some specific
|
||||
considerations for using HCL-based configuration in distributed systems.
|
||||
|
||||
For the sake of this section, we are concerned with distributed systems where
|
||||
at least two separate components both depend on the content of HCL-based
|
||||
configuration files. Real-world examples include the following:
|
||||
|
||||
* **HashiCorp Nomad** loads configuration (job specifications) in its servers
|
||||
but also needs these results in its clients and in its various driver plugins.
|
||||
|
||||
* **HashiCorp Terraform** parses configuration in Terraform Core but can write
|
||||
a partially-evaluated execution plan to disk and continue evaluation in a
|
||||
separate process later. It must also pass configuration values into provider
|
||||
plugins.
|
||||
|
||||
Broadly speaking, there are two approaches to allowing configuration to be
|
||||
accessed in multiple subsystems, which the following subsections will discuss
|
||||
separately.
|
||||
|
||||
Ahead-of-time Evaluation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Ahead-of-time evaluation is the simplest path, with the configuration files
|
||||
being entirely evaluated on entry to the system, and then only the resulting
|
||||
*constant values* being passed between subsystems.
|
||||
|
||||
This approach is relatively straightforward because the resulting
|
||||
:go:type:`cty.Value` results can be losslessly serialized as either JSON or
|
||||
msgpack as long as all system components agree on the expected value types.
|
||||
Aside from passing these values around "on the wire", parsing and decoding of
|
||||
configuration proceeds as normal.
|
||||
|
||||
Both Nomad and Terraform use this approach for interacting with *plugins*,
|
||||
because the plugins themselves are written by various different teams that do
|
||||
not coordinate closely, and so doing all expression evaluation in the core
|
||||
subsystems ensures consistency between plugins and simplifies plugin development.
|
||||
|
||||
In both applications, the plugin is expected to describe (using an
|
||||
application-specific protocol) the schema it expects for each element of
|
||||
configuration it is responsible for, allowing the core subsystems to perform
|
||||
decoding on the plugin's behalf and pass a value that is guaranteed to conform
|
||||
to the schema.
|
||||
|
||||
Gradual Evaluation
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Although ahead-of-time evaluation is relatively straightforward, it has the
|
||||
significant disadvantage that all data available for access via variables or
|
||||
functions must be known by whichever subsystem performs that initial
|
||||
evaluation.
|
||||
|
||||
For example, in Terraform, the "plan" subcommand is responsible for evaluating
|
||||
the configuration and presenting to the user an execution plan for approval, but
|
||||
certain values in that plan cannot be determined until the plan is already
|
||||
being applied, since the specific values used depend on remote API decisions
|
||||
such as the allocation of opaque id strings for objects.
|
||||
|
||||
In Terraform's case, both the creation of the plan and the eventual apply
|
||||
of that plan *both* entail evaluating configuration, with the apply step
|
||||
having a more complete set of input values and thus producing a more complete
|
||||
result. However, this means that Terraform must somehow make the expressions
|
||||
from the original input configuration available to the separate process that
|
||||
applies the generated plan.
|
||||
|
||||
Good usability requires error and warning messages that are able to refer back
|
||||
to specific sections of the input configuration as context for the reported
|
||||
problem, and the best way to achieve this in a distributed system doing
|
||||
gradual evaluation is to send the configuration *source code* between
|
||||
subsystems. This is generally the most compact representation that retains
|
||||
source location information, and will avoid any inconsistency caused by
|
||||
introducing another intermediate serialization.
|
||||
|
||||
In Terraform's, for example, the serialized plan incorporates both the data
|
||||
structure describing the partial evaluation results from the plan phase and
|
||||
the original configuration files that produced those results, which can then
|
||||
be re-evalauated during the apply step.
|
||||
|
||||
In a gradual evaluation scenario, the application should verify correctness of
|
||||
the input configuration as completely as possible at each state. To help with
|
||||
this, :go:pkg:`cty` has the concept of
|
||||
`unknown values <https://github.com/zclconf/go-cty/blob/master/docs/concepts.md#unknown-values-and-the-dynamic-pseudo-type>`_,
|
||||
which can stand in for values the application does not yet know while still
|
||||
retaining correct type information. HCL expression evaluation reacts to unknown
|
||||
values by performing type checking but then returning another unknown value,
|
||||
causing the unknowns to propagate through expressions automatically.
|
||||
|
||||
.. code-block:: go
|
||||
|
||||
ctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"name": cty.UnknownVal(cty.String),
|
||||
"age": cty.UnknownVal(cty.Number),
|
||||
},
|
||||
}
|
||||
val, moreDiags := expr.Value(ctx)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
Each time an expression is re-evaluated with additional information, fewer of
|
||||
the input values will be unknown and thus more of the result will be known.
|
||||
Eventually the application should evaluate the expressions with no unknown
|
||||
values at all, which then guarantees that the result will also be wholly-known.
|
||||
|
||||
Static References, Calls, Lists, and Maps
|
||||
-----------------------------------------
|
||||
|
||||
In most cases, we care more about the final result value of an expression than
|
||||
how that value was obtained. A particular list argument, for example, might
|
||||
be defined by the user via a tuple constructor, by a `for` expression, or by
|
||||
assigning the value of a variable that has a suitable list type.
|
||||
|
||||
In some special cases, the structure of the expression is more important than
|
||||
the result value, or an expression may not *have* a reasonable result value.
|
||||
For example, in Terraform there are a few arguments that call for the user
|
||||
to name another object by reference, rather than provide an object value:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
resource "cloud_network" "example" {
|
||||
# ...
|
||||
}
|
||||
|
||||
resource "cloud_subnet" "example" {
|
||||
cidr_block = "10.1.2.0/24"
|
||||
|
||||
depends_on = [
|
||||
cloud_network.example,
|
||||
]
|
||||
}
|
||||
|
||||
The ``depends_on`` argument in the second ``resource`` block *appears* as an
|
||||
expression that would construct a single-element tuple containing an object
|
||||
representation of the first resource block. However, Terraform uses this
|
||||
expression to construct its dependency graph, and so it needs to see
|
||||
specifically that this expression refers to ``cloud_network.example``, rather
|
||||
than determine a result value for it.
|
||||
|
||||
HCL offers a number of "static analysis" functions to help with this sort of
|
||||
situation. These all live in the :go:pkg:`hcl` package, and each one imposes
|
||||
a particular requirement on the syntax tree of the expression it is given,
|
||||
and returns a result derived from that if the expression conforms to that
|
||||
requirement.
|
||||
|
||||
.. go:currentpackage:: hcl
|
||||
|
||||
.. go:function:: func ExprAsKeyword(expr Expression) string
|
||||
|
||||
This function attempts to interpret the given expression as a single keyword,
|
||||
returning that keyword as a string if possible.
|
||||
|
||||
A "keyword" for the purposes of this function is an expression that can be
|
||||
understood as a valid single identifier. For example, the simple variable
|
||||
reference ``foo`` can be interpreted as a keyword, while ``foo.bar``
|
||||
cannot.
|
||||
|
||||
As a special case, the language-level keywords ``true``, ``false``, and
|
||||
``null`` are also considered to be valid keywords, allowing the calling
|
||||
application to disregard their usual meaning.
|
||||
|
||||
If the given expression cannot be reduced to a single keyword, the result
|
||||
is an empty string. Since an empty string is never a valid keyword, this
|
||||
result unambiguously signals failure.
|
||||
|
||||
.. go:function:: func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics)
|
||||
|
||||
This is a generalization of ``ExprAsKeyword`` that will accept anything that
|
||||
can be interpreted as a *traversal*, which is a variable name followed by
|
||||
zero or more attribute access or index operators with constant operands.
|
||||
|
||||
For example, all of ``foo``, ``foo.bar`` and ``foo[0]`` are valid
|
||||
traversals, but ``foo[bar]`` is not, because the ``bar`` index is not
|
||||
constant.
|
||||
|
||||
This is the function that Terraform uses to interpret the items within the
|
||||
``depends_on`` sequence in our example above.
|
||||
|
||||
As with ``ExprAsKeyword``, this function has a special case that the
|
||||
keywords ``true``, ``false``, and ``null`` will be accepted as if they were
|
||||
variable names by this function, allowing ``null.foo`` to be interpreted
|
||||
as a traversal even though it would be invalid if evaluated.
|
||||
|
||||
If error diagnostics are returned, the traversal result is invalid and
|
||||
should not be used.
|
||||
|
||||
.. go:function:: func RelTraversalForExpr(expr Expression) (Traversal, Diagnostics)
|
||||
|
||||
This is very similar to ``AbsTraversalForExpr``, but the result is a
|
||||
*relative* traversal, which is one whose first name is considered to be
|
||||
an attribute of some other (implied) object.
|
||||
|
||||
The processing rules are identical to ``AbsTraversalForExpr``, with the
|
||||
only exception being that the first element of the returned traversal is
|
||||
marked as being an attribute, rather than as a root variable.
|
||||
|
||||
.. go:function:: func ExprList(expr Expression) ([]Expression, Diagnostics)
|
||||
|
||||
This function requires that the given expression be a tuple constructor,
|
||||
and if so returns a slice of the element expressions in that constructor.
|
||||
Applications can then perform further static analysis on these, or evaluate
|
||||
them as normal.
|
||||
|
||||
If error diagnostics are returned, the result is invalid and should not be
|
||||
used.
|
||||
|
||||
This is the fucntion that Terraform uses to interpret the expression
|
||||
assigned to ``depends_on`` in our example above, then in turn using
|
||||
``AbsTraversalForExpr`` on each enclosed expression.
|
||||
|
||||
.. go:function:: func ExprMap(expr Expression) ([]KeyValuePair, Diagnostics)
|
||||
|
||||
This function requires that the given expression be an object constructor,
|
||||
and if so returns a slice of the element key/value pairs in that constructor.
|
||||
Applications can then perform further static analysis on these, or evaluate
|
||||
them as normal.
|
||||
|
||||
If error diagnostics are returned, the result is invalid and should not be
|
||||
used.
|
||||
|
||||
.. go:function:: func ExprCall(expr Expression) (*StaticCall, Diagnostics)
|
||||
|
||||
This function requires that the given expression be a function call, and
|
||||
if so returns an object describing the name of the called function and
|
||||
expression objects representing the call arguments.
|
||||
|
||||
If error diagnostics are returned, the result is invalid and should not be
|
||||
used.
|
||||
|
||||
The ``Variables`` method on :go:type:`hcl.Expression` is also considered to be
|
||||
a "static analysis" helper, but is built in as a fundamental feature because
|
||||
analysis of referenced variables is often important for static validation and
|
||||
for implementing interdependent blocks as we saw in the section above.
|
||||
|
35
guide/index.rst
Normal file
35
guide/index.rst
Normal file
@ -0,0 +1,35 @@
|
||||
HCL Configuration Language
|
||||
==========================
|
||||
|
||||
HCL is a toolkit for creating structured configuration languages that are both
|
||||
human- and machine-friendly, for use with command-line tools, servers, etc.
|
||||
|
||||
HCL has both a native syntax, intended to be pleasant to read and write for
|
||||
humans, and a JSON-based variant that is easier for machines to generate and
|
||||
parse. The native syntax is inspired by libucl_, `nginx configuration`_, and
|
||||
others.
|
||||
|
||||
It includes an expression syntax that allows basic inline computation and, with
|
||||
support from the calling application, use of variables and functions for more
|
||||
dynamic configuration languages.
|
||||
|
||||
HCL provides a set of constructs that can be used by a calling application to
|
||||
construct a configuration language. The application defines which argument
|
||||
names and nested block types are expected, and HCL parses the configuration
|
||||
file, verifies that it conforms to the expected structure, and returns
|
||||
high-level objects that the application can use for further processing.
|
||||
|
||||
At present, HCL is primarily intended for use in applications written in Go_,
|
||||
via its library API.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Contents:
|
||||
|
||||
intro
|
||||
go
|
||||
language_design
|
||||
|
||||
.. _libucl: https://github.com/vstakhov/libucl
|
||||
.. _`nginx configuration`: http://nginx.org/en/docs/beginners_guide.html#conf_structure
|
||||
.. _Go: https://golang.org/
|
108
guide/intro.rst
Normal file
108
guide/intro.rst
Normal file
@ -0,0 +1,108 @@
|
||||
.. _intro:
|
||||
|
||||
Introduction to HCL
|
||||
===================
|
||||
|
||||
HCL-based configuration is built from two main constructs: arguments and
|
||||
blocks. The following is an example of a configuration language for a
|
||||
hypothetical application:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
io_mode = "async"
|
||||
|
||||
service "http" "web_proxy" {
|
||||
listen_addr = "127.0.0.1:8080"
|
||||
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
|
||||
process "mgmt" {
|
||||
command = ["/usr/local/bin/awesome-app", "mgmt"]
|
||||
}
|
||||
}
|
||||
|
||||
In the above example, ``io_mode`` is a top-level argument, while ``service``
|
||||
introduces a block. Within the body of a block, further arguments and nested
|
||||
blocks are allowed. A block type may also expect a number of *labels*, which
|
||||
are the quoted names following the ``service`` keyword in the above example.
|
||||
|
||||
The specific keywords ``io_mode``, ``service``, ``process``, etc here are
|
||||
application-defined. HCL provides the general block structure syntax, and
|
||||
can validate and decode configuration based on the application's provided
|
||||
schema.
|
||||
|
||||
HCL is a structured configuration language rather than a data structure
|
||||
serialization language. This means that unlike languages such as JSON, YAML,
|
||||
or TOML, HCL is always decoded using an application-defined schema.
|
||||
|
||||
However, HCL does have a JSON-based alternative syntax, which allows the same
|
||||
structure above to be generated using a standard JSON serializer when users
|
||||
wish to generate configuration programmatically rather than hand-write it:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"io_mode": "async",
|
||||
"service": {
|
||||
"http": {
|
||||
"web_proxy": {
|
||||
"listen_addr": "127.0.0.1:8080",
|
||||
"process": {
|
||||
"main": {
|
||||
"command": ["/usr/local/bin/awesome-app", "server"]
|
||||
},
|
||||
"mgmt": {
|
||||
"command": ["/usr/local/bin/awesome-app", "mgmt"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
The calling application can choose which syntaxes to support. JSON syntax may
|
||||
not be important or desirable for certain applications, but it is available for
|
||||
applications that need it. The schema provided by the calling application
|
||||
allows JSON input to be properly decoded even though JSON syntax is ambiguous
|
||||
in various ways, such as whether a JSON object is representing a nested block
|
||||
or an object expression.
|
||||
|
||||
The collection of arguments and blocks at a particular nesting level is called
|
||||
a *body*. A file always has a root body containing the top-level elements,
|
||||
and each block also has its own body representing the elements within it.
|
||||
|
||||
The term "attribute" can also be used to refer to what we've called an
|
||||
"argument" so far. The term "attribute" is also used for the fields of an
|
||||
object value in argument expressions, and so "argument" is used to refer
|
||||
specifically to the type of attribute that appears directly within a body.
|
||||
|
||||
The above examples show the general "texture" of HCL-based configuration. The
|
||||
full details of the syntax are covered in the language specifications.
|
||||
|
||||
.. todo:: Once the language specification documents have settled into a
|
||||
final location, link them from above.
|
||||
|
||||
Argument Expressions
|
||||
--------------------
|
||||
|
||||
The value of an argument can be a literal value shown above, or it may be an
|
||||
expression to allow arithmetic, deriving one value from another, etc.
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
listen_addr = env.LISTEN_ADDR
|
||||
|
||||
Built-in arithmetic and comparison operators are automatically available in all
|
||||
HCL-based configuration languages. A calling application may optionally
|
||||
provide variables that users can reference, like ``env`` in the above example,
|
||||
and custom functions to transform values in application-specific ways.
|
||||
|
||||
Full details of the expression syntax are in the HCL native syntax
|
||||
specification. Since JSON does not have an expression syntax, JSON-based
|
||||
configuration files use the native syntax expression language embedded inside
|
||||
JSON strings.
|
||||
|
||||
.. todo:: Once the language specification documents have settled into a
|
||||
final location, link to the native syntax specification from above.
|
285
guide/language_design.rst
Normal file
285
guide/language_design.rst
Normal file
@ -0,0 +1,285 @@
|
||||
Configuration Language Design
|
||||
=============================
|
||||
|
||||
In this section we will cover some conventions for HCL-based configuration
|
||||
languages that can help make them feel consistent with other HCL-based
|
||||
languages, and make the best use of HCL's building blocks.
|
||||
|
||||
HCL's native and JSON syntaxes both define a mapping from input bytes to a
|
||||
higher-level information model. In designing a configuration language based on
|
||||
HCL, your building blocks are the components in that information model:
|
||||
blocks, arguments, and expressions.
|
||||
|
||||
Each calling application of HCL, then, effectively defines its own language.
|
||||
Just as Atom and RSS are higher-level languages built on XML, HashiCorp
|
||||
Terraform has a higher-level language built on HCL, while HashiCorp Nomad has
|
||||
its own distinct language that is *also* built on HCL.
|
||||
|
||||
From an end-user perspective, these are distinct languages but have a common
|
||||
underlying texture. Users of both are therefore likely to bring some
|
||||
expectations from one to the other, and so this section is an attempt to
|
||||
codify some of these shared expectations to reduce user surprise.
|
||||
|
||||
These are subjective guidelines however, and so applications may choose to
|
||||
ignore them entirely or ignore them in certain specialized cases. An
|
||||
application providing a configuration language for a pre-existing system, for
|
||||
example, may choose to eschew the identifier naming conventions in this section
|
||||
in order to exactly match the existing names in that underlying system.
|
||||
|
||||
Language Keywords and Identifiers
|
||||
---------------------------------
|
||||
|
||||
Much of the work in defining an HCL-based language is in selecting good names
|
||||
for arguments, block types, variables, and functions.
|
||||
|
||||
The standard for naming in HCL is to use all-lowercase identifiers with
|
||||
underscores separating words, like ``service`` or ``io_mode``. HCL identifiers
|
||||
do allow uppercase letters and dashes, but this primarily for natural
|
||||
interfacing with external systems that may have other identifier conventions,
|
||||
and so these should generally be avoided for the identifiers native to your
|
||||
own language.
|
||||
|
||||
The distinction between "keywords" and other identifiers is really just a
|
||||
convention. In your own language documentation, you may use the word "keyword"
|
||||
to refer to names that are presented as an intrinsic part of your language,
|
||||
such as important top-level block type names.
|
||||
|
||||
Block type names are usually singular, since each block defines a single
|
||||
object. Use a plural block name only if the block is serving only as a
|
||||
namespacing container for a number of other objects. A block with a plural
|
||||
type name will generally contain only nested blocks, and no arguments of its
|
||||
own.
|
||||
|
||||
Argument names are also singular unless they expect a collection value, in
|
||||
which case they should be plural. For example, ``name = "foo"`` but
|
||||
``subnet_ids = ["abc", "123"]``.
|
||||
|
||||
Function names will generally *not* use underscores and will instead just run
|
||||
words together, as is common in the C standard library. This is a result of
|
||||
the fact that several of the standard library functions offered in ``cty``
|
||||
(covered in a later section) have names that follow C library function names
|
||||
like ``substr``. This is not a strong rule, and applications that use longer
|
||||
names may choose to use underscores for them to improve readability.
|
||||
|
||||
Blocks vs. Object Values
|
||||
------------------------
|
||||
|
||||
HCL blocks and argument values of object type have quite a similar appearance
|
||||
in the native syntax, and are identical in JSON syntax:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
block {
|
||||
foo = bar
|
||||
}
|
||||
|
||||
# argument with object constructor expression
|
||||
argument = {
|
||||
foo = bar
|
||||
}
|
||||
|
||||
In spite of this superficial similarity, there are some important differences
|
||||
between these two forms.
|
||||
|
||||
The most significant difference is that a child block can contain nested blocks
|
||||
of its own, while an object constructor expression can define only attributes
|
||||
of the object it is creating.
|
||||
|
||||
The user-facing model for blocks is that they generally form the more "rigid"
|
||||
structure of the language itself, while argument values can be more free-form.
|
||||
An application will generally define in its schema and documentation all of
|
||||
the arguments that are valid for a particular block type, while arguments
|
||||
accepting object constructors are more appropriate for situations where the
|
||||
arguments themselves are freely selected by the user, such as when the
|
||||
expression will be converted by the application to a map type.
|
||||
|
||||
As a less contrived example, consider the ``resource`` block type in Terraform
|
||||
and its use with a particular resource type ``aws_instance``:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
resource "aws_instance" "example" {
|
||||
ami = "ami-abc123"
|
||||
instance_type = "t2.micro"
|
||||
|
||||
tags = {
|
||||
Name = "example instance"
|
||||
}
|
||||
|
||||
ebs_block_device {
|
||||
device_name = "hda1"
|
||||
volume_size = 8
|
||||
volume_type = "standard"
|
||||
}
|
||||
}
|
||||
|
||||
The top-level block type ``resource`` is fundamental to Terraform itself and
|
||||
so an obvious candidate for block syntax: it maps directly onto an object in
|
||||
Terraform's own domain model.
|
||||
|
||||
Within this block we see a mixture of arguments and nested blocks, all defined
|
||||
as part of the schema of the ``aws_instance`` resource type. The ``tags``
|
||||
map here is specified as an argument because its keys are free-form, chosen
|
||||
by the user and mapped directly onto a map in the underlying system.
|
||||
``ebs_block_device`` is specified as a nested block, because it is a separate
|
||||
domain object within the remote system and has a rigid schema of its own.
|
||||
|
||||
As a special case, block syntax may sometimes be used with free-form keys if
|
||||
those keys each serve as a separate declaration of some first-class object
|
||||
in the language. For example, Terraform has a top-level block type ``locals``
|
||||
which behaves in this way:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
locals {
|
||||
instance_type = "t2.micro"
|
||||
instance_id = aws_instance.example.id
|
||||
}
|
||||
|
||||
Although the argument names in this block are arbitrarily selected by the
|
||||
user, each one defines a distinct top-level object. In other words, this
|
||||
approach is used to create a more ergonomic syntax for defining these simple
|
||||
single-expression objects, as a pragmatic alternative to more verbose and
|
||||
redundant declarations using blocks:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
local "instance_type" {
|
||||
value = "t2.micro"
|
||||
}
|
||||
local "instance_id" {
|
||||
value = aws_instance.example.id
|
||||
}
|
||||
|
||||
The distinction between domain objects, language constructs and user data will
|
||||
always be subjective, so the final decision is up to you as the language
|
||||
designer.
|
||||
|
||||
Standard Functions
|
||||
------------------
|
||||
|
||||
HCL itself does not define a common set of functions available in all HCL-based
|
||||
languages; the built-in language operators give a baseline of functionality
|
||||
that is always available, but applications are free to define functions as they
|
||||
see fit.
|
||||
|
||||
With that said, there's a number of generally-useful functions that don't
|
||||
belong to the domain of any one application: string manipulation, sequence
|
||||
manipulation, date formatting, JSON serialization and parsing, etc.
|
||||
|
||||
Given the general need such functions serve, it's helpful if a similar set of
|
||||
functions is available with compatible behavior across multiple HCL-based
|
||||
languages, assuming the language is for an application where function calls
|
||||
make sense at all.
|
||||
|
||||
The Go implementation of HCL is built on an underlying type and function system
|
||||
:go:pkg:`cty`, whose usage was introduced in :ref:`go-expression-funcs`. That
|
||||
library also has a package of "standard library" functions which we encourage
|
||||
applications to offer with consistent names and compatible behavior, either by
|
||||
using the standard implementations directly or offering compatible
|
||||
implementations under the same name.
|
||||
|
||||
The "standard" functions that new configuration formats should consider
|
||||
offering are:
|
||||
|
||||
* ``abs(number)`` - returns the absolute (positive) value of the given number.
|
||||
* ``coalesce(vals...)`` - returns the value of the first argument that isn't null. Useful only in formats where null values may appear.
|
||||
* ``compact(vals...)`` - returns a new tuple with the non-null values given as arguments, preserving order.
|
||||
* ``concat(seqs...)`` - builds a tuple value by concatenating together all of the given sequence (list or tuple) arguments.
|
||||
* ``format(fmt, args...)`` - performs simple string formatting similar to the C library function ``printf``.
|
||||
* ``hasindex(coll, idx)`` - returns true if the given collection has the given index. ``coll`` may be of list, tuple, map, or object type.
|
||||
* ``int(number)`` - returns the integer component of the given number, rounding towards zero.
|
||||
* ``jsondecode(str)`` - interprets the given string as JSON format and return the corresponding decoded value.
|
||||
* ``jsonencode(val)`` - encodes the given value as a JSON string.
|
||||
* ``length(coll)`` - returns the length of the given collection.
|
||||
* ``lower(str)`` - converts the letters in the given string to lowercase, using Unicode case folding rules.
|
||||
* ``max(numbers...)`` - returns the highest of the given number values.
|
||||
* ``min(numbers...)`` - returns the lowest of the given number values.
|
||||
* ``sethas(set, val)`` - returns true only if the given set has the given value as an element.
|
||||
* ``setintersection(sets...)`` - returns the intersection of the given sets
|
||||
* ``setsubtract(set1, set2)`` - returns a set with the elements from ``set1`` that are not also in ``set2``.
|
||||
* ``setsymdiff(sets...)`` - returns the symmetric difference of the given sets.
|
||||
* ``setunion(sets...)`` - returns the union of the given sets.
|
||||
* ``strlen(str)`` - returns the length of the given string in Unicode grapheme clusters.
|
||||
* ``substr(str, offset, length)`` - returns a substring from the given string by splitting it between Unicode grapheme clusters.
|
||||
* ``timeadd(time, duration)`` - takes a timestamp in RFC3339 format and a possibly-negative duration given as a string like ``"1h"`` (for "one hour") and returns a new RFC3339 timestamp after adding the duration to the given timestamp.
|
||||
* ``upper(str)`` - converts the letters in the given string to uppercase, using Unicode case folding rules.
|
||||
|
||||
Not all of these functions will make sense in all applications. For example, an
|
||||
application that doesn't use set types at all would have no reason to provide
|
||||
the set-manipulation functions here.
|
||||
|
||||
Some languages will not provide functions at all, since they are primarily for
|
||||
assigning values to arguments and thus do not need nor want any custom
|
||||
computations of those values.
|
||||
|
||||
Block Results as Expression Variables
|
||||
-------------------------------------
|
||||
|
||||
In some applications, top-level blocks serve also as declarations of variables
|
||||
(or of attributes of object variables) available during expression evaluation,
|
||||
as discussed in :ref:`go-interdep-blocks`.
|
||||
|
||||
In this case, it's most intuitive for the variables map in the evaluation
|
||||
context to contain an value named after each valid top-level block
|
||||
type and for these values to be object-typed or map-typed and reflect the
|
||||
structure implied by block type labels.
|
||||
|
||||
For example, an application may have a top-level ``service`` block type
|
||||
used like this:
|
||||
|
||||
.. code-block:: hcl
|
||||
|
||||
service "http" "web_proxy" {
|
||||
listen_addr = "127.0.0.1:8080"
|
||||
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
|
||||
process "mgmt" {
|
||||
command = ["/usr/local/bin/awesome-app", "mgmt"]
|
||||
}
|
||||
}
|
||||
|
||||
If the result of decoding this block were available for use in expressions
|
||||
elsewhere in configuration, the above convention would call for it to be
|
||||
available to expressions as an object at ``service.http.web_proxy``.
|
||||
|
||||
If it the contents of the block itself that are offered to evaluation -- or
|
||||
a superset object *derived* from the block contents -- then the block arguments
|
||||
can map directly to object attributes, but it is up to the application to
|
||||
decide which value type is most appropriate for each block type, since this
|
||||
depends on how multiple blocks of the same type relate to one another, or if
|
||||
multiple blocks of that type are even allowed.
|
||||
|
||||
In the above example, an application would probably expose the ``listen_addr``
|
||||
argument value as ``service.http.web_proxy.listen_addr``, and may choose to
|
||||
expose the ``process`` blocks as a map of objects using the labels as keys,
|
||||
which would allow an expression like
|
||||
``service.http.web_proxy.service["main"].command``.
|
||||
|
||||
If multiple blocks of a given type do not have a significant order relative to
|
||||
one another, as seems to be the case with these ``process`` blocks,
|
||||
representation as a map is often the most intuitive. If the ordering of the
|
||||
blocks *is* significant then a list may be more appropriate, allowing the use
|
||||
of HCL's "splat operators" for convenient access to child arguments. However,
|
||||
there is no one-size-fits-all solution here and language designers must
|
||||
instead consider the likely usage patterns of each value and select the
|
||||
value representation that best accommodates those patterns.
|
||||
|
||||
Some applications may choose to offer variables with slightly different names
|
||||
than the top-level blocks in order to allow for more concise references, such
|
||||
as abbreviating ``service`` to ``svc`` in the above examples. This should be
|
||||
done with care since it may make the relationship between the two less obvious,
|
||||
but this may be a good tradeoff for names that are accessed frequently that
|
||||
might otherwise hurt the readability of expressions they are embedded in.
|
||||
Familiarity permits brevity.
|
||||
|
||||
Many applications will not make blocks results available for use in other
|
||||
expressions at all, in which case they are free to select whichever variable
|
||||
names make sense for what is being exposed. For example, a format may make
|
||||
environment variable values available for use in expressions, and may do so
|
||||
either as top-level variables (if no other variables are needed) or as an
|
||||
object named ``env``, which can be used as in ``env.HOME``.
|
||||
|
36
guide/make.bat
Normal file
36
guide/make.bat
Normal file
@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=HCL
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
3
guide/requirements.txt
Normal file
3
guide/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
sphinx
|
||||
sphinxcontrib-golangdomain
|
||||
sphinx-autoapi
|
Loading…
Reference in New Issue
Block a user