Skip to content

Value Passing

Rebecca Skinner edited this page Aug 30, 2016 · 5 revisions

Value Passing as a Template Language Construct

Value passing will be handled at Preparer time during template execution through the use of the lookup template function that, which allows access into module-specific structure fields.

Depending on the specific fields that are being references and when they are being generated by the origin module they may not be available during different processing phases. The specific phases are outlined below.

Cross-Referencing (prepare time)

Cross-referencing functions similar to parameters in that it allows access to fields of a node that are defined within the module. For example in xref example we access the destination of file.content.config which is a statically accessible value that will be available at prepare time.

file.content "config" {
  destination = "/etc/config/config.conf"
  contents = "config file data"
}

task {
  check = "[[ -f output]]"
  apply = "echo -n 'filename: {{lookup `file.content.config.destination`}}' | tee -a output"
}

Read-effect References (check time)

Read-effect references are values that can only be computed at runtime but whose computation does not have an affect on the running system. These are values that can be checked during planning and a referenced with the lookup function as show below.

param "iface" {
  default = "en0"
}

task "ipaddr" {
  query = "ifconfig {{param `iface`}} | grep 'inet\W' | awk '{print $2}'"
}

file.content "address" {
  destination="myaddress"
  content="{{lookup `task.once.ipaddr.stdout`}}"
}

Effect References (apply time)

Effect references are values that are only available after an effectful operation has been completed. They are accessible with the lookup function as shown below.

task "effectful" {
  check = "[[ -f random.md5 ]]"
  apply = "dd if=/dev/random bs=512 count=1 2>/dev/null | md5 > random.md5"
}

file.content "checksum" {
  destination = "/etc/checksums/{{results `task.effectful.stdout`}}"
  content = "{{lookup `task.effectful.stdout`}}"
}

Value-Passing Semantics

Call Count and Ordering Guarantees

Any call to `lookup` will be memoized for the duration of the execution. Calls to `lookup` will always reference the most recent call to `Check` or `Apply` first and will traverse the call order until no more `TaskStatus` results are found. If the end of the call list is encountered before a struct field match is found then the node is unresolvable.

Unresolvable Nodes

All nodes are considered unresolvable at load time. A node will be resolvable if all of it's value dependency calls are resolvable at the current operation level. The table below outlines which converge operations support value reference types.

#+name

Table 1: Converge calls and associated defined behavior for value passing
  Defined during `Prepare` Defined during `Check` Defined during `Apply`
`healthcheck` defined defined undefined
`plan` defined defined undefined
`apply` defined defined defined
`graph`1 defined1 defined1 defined1

The system behavior with relation to undefined nodes are shown as follows:

  • healthcheck: Healthcheck will evaluate xref and load calls. Any unresolvable nodes will be put in a warning state.
  • plan: Plan will evaluate xref and load calls. Effectful dependencies from results will be displayed as unresolvable as show in Ex 1.
  • apply: Because apply will perform effectful operations all value passing functions are supported.
  • graph: Nodes are displayed irrespective of references, but edges are shown for value passing dependencies.

#+name

root/file.content.checksum output:  Unresolvable

Summary: 0 errors, 1 changes
1 node(s) depend on effectful results and may change

Value Passing Internals:

This section describes the internal implementation of value passing with an emphasis on understanding the requirements to support value passing at the per-module level.

API Changes

Render Interfaces

Package render will now export an error ErrUnresolvable that will be returned by Renderer.Render() if the template attempts to reference an unresolvable element of a node.

Task Interfaces

The resource.Task interface will have minor modifications to support value passing. The interface will now be defined as shown in 3.1.2. Of particular note is that both Check and Apply will now be supplied with a Renderer to facilitate deferred rendering.

#+name

// Task will now pass a renderer to Check and Apply, Apply now returns a
// TaskStatus
type Task interface {
        Check(Renderer) (TaskStatus, error)
        Apply(Renderer) (TaskStatus, error)
}

// Resource adds metadata about the executed tasks
type Resource interface {
        Prepare(Renderer) (Task, error)
}

// Renderer is passed to resources
type Renderer interface {
        Value() (value string, present bool)
        Render(name, content string) (string, error)
}

Exposing Values for Passing

Module specific implementations of Task and TaskStatus are used for exposing fields to other modules at runtime. Structure fields that are present in the modules' preparer and task status structs will be made available automatically. nil values are used to indicate unresolvable values.

The resource.Task implementation returned by Prepare is the base value that should contain any any statically resolvable data such data specified in the HCL document or constants defined by the module.

Note that the TaskStatus returned by Check() and Apply() should expose the same values in order to ensure that valid but unresolvable paths are differentiated from erroneous paths at template execution time. These values should be initialized by Prepare, which will continue to be initialized by raw parsed hcl data during loading.

Evaluation Ordering

Each of the load, plan or health check, and apply phases are considered during field reference resolution. Fields are considered in order with priority given to the most recent exceution, except during application where the results of Apply() are considered before the results of the subsequent secondary Check() call.

+------------------------------------+----------------------+--------------+
|             Apply Phase            |  Plan / Healthcheck  |  Load Phase  |
+------------------------------------+----------------------+--------------+
| Task Status   ---->  Task Status --+-----> Task Status ---+-->   Task    |
|   (Apply)              (Check)     |        (Check)       |    (Prepare) |
+------------------------------------+----------------------+--------------+

Tips on Exposing Values

Modules may continue to use the existing resource.Status structure as a default implementation of TaskStatus, however the exposed values from resource.Status are insufficient for useful value passing. As such modules should embed the resource.Status structure in a richer structure that contains the exposed fields.

Modules may also choose to use intermediary structures to assist in differentiating between values availble during different phases of operation, as shown in the example below:

type CheckStatus struct {
        Value1 string
        Value2 string
}

type ApplyStatus struct {
        Value1 string
        Value3 string
}

type Example struct {
        *CheckStatus
        *ApplyStatus
        *resource.Status
}

this approach allows a user to treat execution phases are namespaces as in 3.2.2. This approach also may reduce errors related to unresolvable values.

\#+beginsrc hcl pre = "{{example.CheckStatus.Value1}}" post = "{{example.ApplyStatus.Value3}}" \#+endsrc hcl

Unresolvable References

Unresolvable references occur when a field referenced in a node is not defined until an execution phase greater than the maximum specified current execution phase. E.g. during planning any references to values defined by apply are unresolvable.

Maximizing Resolvability With Deferred Rendering

Unresolvability can be minmized by deferring template rendering until the last possible minute. Specifically by rendering strings only using during a modules Apply() phase during the call to Apply instead of during Prepare templates may remain resolvable during a larger set of run conditions.

To facilitate this the interface to Check() and Apply() has been expanded to support a Renderer as a parameter.