Terraform
Paths
An exact location within a schema or schema-based data such as tfsdk.Config
, tfsdk.Plan
, or tfsdk.State
, is referred to as a path.
Usage
Example uses in the framework include:
- Diagnostics intended for a specific attribute, which allows Terraform to show the configuration file and line information.
- Accessing configuration, plan, or state data for a specific attribute.
- Writing plan or state data for a specific attribute.
More advanced use cases, such as path based attribute validators, typically implement path expressions which enables additional logic beyond exact paths.
Concepts
Paths are designed around the underlying Terraform implementation of a schema and schema-based data. Terraform schemas are a tree structure based on value storing attributes, which may have their own structural component, and structural blocks. Both attributes and blocks are associated with specific data types, some of which represent structural or collection types which hold further information which can be traversed. Each traversal into that further information is referred to as a path step. Paths are always absolute and start from the root, or top level, of a schema.
Given the tree structure of Terraform schemas, descriptions of paths and their steps borrow certain hierarchy terminology such as parent and child. A parent path describes a path without one or more of the final steps of a given path, or put differently, a partial path closer to the root of the schema. A child path describes a path with one or more additional steps beyond a given path, or put differently, a path containing the given path but further from the root of the schema.
Every path must align with the schema definition or an error diagnostic will be raised when working with paths within the framework. Provider-defined functionality that is schema-based, such as attribute validation and attribute plan modification, are provided an accurate current path since that functionality would not be able to determine its own path.
Path expressions are an abstraction on top of paths, which enable additional use cases with provider-defined functionality, such as relative path, parent step, and wildcard step support.
Building Paths
The framework implementation for paths is in the path
package, with the path.Path
type being the main provider developer interaction point. Call the path.Root()
function with an attribute name or block name at the root of the schema to begin a path.
Given this example schema with a root attribute named example_root_attribute
:
schema.Schema{
Attributes: map[string]schema.Attribute{
"example_root_attribute": schema.StringAttribute{
Required: true,
},
},
}
The call to path.Root()
which matches the location of example_root_attribute
string value is:
path.Root("example_root_attribute")
For blocks, the beginning of a path is similarly defined. Attribute and block names cannot overlap, so the framework automatically handles whether a path is referring to an attribute or block to start.
Given this example schema with a root block named example_root_block
:
schema.Schema{
Blocks: map[string]schema.Block{
"example_root_block": schema.ListNestedBlock{
NestedObject: schema.NestedBlockObject{/* ... */},
},
},
}
The call to path.Root()
which matches the location of example_root_block
list value is:
path.Root("example_root_block")
Once a path.Path
is started, it supports a builder pattern, which allows for chaining method calls to construct a full path.
This example shows a hypothetical path that points to the first element of a list attribute to highlight the builder pattern:
path.Root("example_list_attribute").AtListIndex(0)
This pattern can be extended to as many calls as necessary. The different framework schema types and their associated path step methods are shown in the following sections.
Building Attribute Paths
The following table shows the different path.Path
type methods associated with building paths for attribute implementations. Attribute types that cannot be traversed further are shown with N/A (not applicable).
Attribute Type | Child Path Method |
---|---|
schema.BoolAttribute | N/A |
schema.DynamicAttribute | N/A |
schema.Float32Attribute | N/A |
schema.Float64Attribute | N/A |
schema.Int32Attribute | N/A |
schema.Int64Attribute | N/A |
schema.ListAttribute | AtListIndex() |
schema.MapAttribute | AtMapKey() |
schema.NumberAttribute | N/A |
schema.ObjectAttribute | AtName() |
schema.SetAttribute | AtSetValue() |
schema.StringAttribute | N/A |
Given following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_map_attribute": schema.MapAttribute{
ElementType: types.StringType,
Required: true,
},
},
}
The path which matches the string value associated with a hypothetical map key of example-key
of the root_map_attribute
attribute is:
path.Root("root_map_attribute").AtMapKey("example-key")
Any type that supports a child path may define an element type that also supports a child path. Paths can continue to be built using the associated method with each level of the attribute type.
Given following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_map_attribute": schema.MapAttribute{
ElementType: types.ListType{
ElemType: types.StringType,
},
Required: true,
},
},
}
The path which matches the third string value associated with the list value of a hypothetical map key of example-key
of the root_map_attribute
attribute is:
path.Root("root_map_attribute").AtMapKey("example-key").AtListIndex(2)
Unless there is a very well defined data structure involved, the level of path specificity in the example above is fairly uncommon to manually build in provider logic though. Most provider logic will typically build a path to the value of an attribute (e.g. its first type) and work with that data, or potentially one level deeper in well known cases, since each level introduces additional complexity associated with potentially null or unknown values. Provider logic can instead use an iterative path building approach when dealing with attributes that have multiple levels.
This example shows an iterative path building approach to handle any map keys and list indices in the above schema:
attributePath := path.Root("root_map_attribute")
// attributeValue is an example types.Map value which was previously fetched,
// potentially using the path above.
for mapKey, mapValue := range attributeValue.Elements() {
mapKeyPath := attributePath.AtMapKey(mapKey)
// ...
for listIndex, listValue := range mapValue.Elements() {
listIndexPath := mapKeyPath.AtListIndex(listIndex)
// ...
}
}
Building Nested Attribute Paths
The following table shows the different path.Path
type methods associated with building paths for nested attributes.
Nested Attribute Type | Child Path Method |
---|---|
schema.ListNestedAttribute | AtListIndex() |
schema.MapNestedAttribute | AtMapKey() |
schema.SetNestedAttribute | AtSetValue() |
schema.SingleNestedAttribute | AtName() |
Nested attributes eventually implement attributes at child paths, which follow the methods shown in the Building Attribute Paths section.
Building List Nested Attributes Paths
An attribute that implements schema.ListNestedAttribute
conceptually is a list containing objects with attribute names.
Given the following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_list_attribute": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"nested_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
Required: true,
},
},
}
The path which matches the list associated with the root_list_attribute
attribute is:
path.Root("root_list_attribute")
The path which matches the first object in the list associated with the root_list_attribute
attribute is:
path.Root("root_list_attribute").AtListIndex(0)
The path which matches the nested_string_attribute
string value in the first object in the list associated with root_list_attribute
attribute is:
path.Root("root_list_attribute").AtListIndex(0).AtName("nested_string_attribute")
Building Map Nested Attributes Paths
An attribute that implements schema.MapNestedAttribute
conceptually is a map containing values of objects with attribute names.
Given the following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_map_attribute": schema.MapNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"nested_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
Required: true,
},
},
}
The path which matches the map associated with the root_map_attribute
attribute is:
path.Root("root_map_attribute")
The path which matches the a hypothetical "example-key"
object in the map associated with the root_map_attribute
attribute is:
path.Root("root_map_attribute").AtMapKey("example-key")
The path which matches the nested_string_attribute
string value in a hypothetical "example-key"
object in the map associated with root_map_attribute
attribute is:
path.Root("root_map_attribute").AtMapKey("example-key").AtName("nested_string_attribute")
Building Set Nested Attributes Paths
An attribute that implements schema.SetNestedAttribute
conceptually is a set containing objects with attribute names. Attempting to build set nested attribute paths is extremely tedius as set element selection is based on the entire value of the element, including any null or unknown values. Avoid manual set-based path building. Instead, use functionality which supports path expressions as that supports wildcard path matching (path.Expression
type AtAnySetValue()
method) or relative paths.
Given the following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_set_attribute": schema.SetNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"nested_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
Required: true,
},
},
}
The path which matches the set associated with the root_set_attribute
attribute is:
path.Root("root_set_attribute")
Examples below will presume a nested_string_attribute
string value of types.StringValue("example")
for brevity. In real world usage, the string value may be types.StringNull()
, types.StringUnknown()
or types.StringValue("something-else")
, which are all considered different set paths from each other. Each additional attribute or block introduces exponentially more possible paths given each attribute or block value may be null, unknown, or a unique known value.
The path which matches the object associated with the root_set_attribute
block is:
path.Root("root_set_attribute").AtSetValue(types.ObjectValueMust(
map[string]attr.Type{
"nested_string_attribute": types.StringType,
},
map[string]attr.Value{
"nested_string_attribute": types.StringValue("example"),
}
))
The path which matches the nested_string_attribute
string value associated with root_set_attribute
block is:
path.Root("root_set_attribute").AtSetValue(types.ObjectValueMust(
map[string]attr.Type{
"nested_string_attribute": types.StringType,
},
map[string]attr.Value{
"nested_string_attribute": types.StringValue("example"),
}
)).AtName("nested_string_attribute")
Building Single Nested Attributes Paths
An attribute that implements schema.SingleNestedAttribute
conceptually is an object with attribute names.
Given the following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_grouped_attributes": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"nested_string_attribute": schema.StringAttribute{
Required: true,
},
},
Required: true,
},
},
}
The path which matches the object associated with the root_grouped_attributes
attribute is:
path.Root("root_grouped_attributes")
The path which matches the nested_string_attribute
string value in the object associated with the root_grouped_attributes
attribute is:
path.Root("root_grouped_attributes").AtName("nested_string_attribute")
Building Block Paths
The following table shows the different path.Path
type methods associated with building paths for blocks.
Block Type | Child Path Method |
---|---|
schema.ListNestedBlock | AtListIndex() |
schema.SetNestedBlock | AtSetValue() |
schema.SingleNestedBlock | AtName() |
Blocks can implement nested blocks. Paths can continue to be built using the associated method with each level of the block type.
Blocks eventually implement attributes at child paths, which follow the methods shown in the Building Attribute Paths section. Blocks cannot contain nested attributes.
Building List Block Paths
A block defined with schema.ListNestedBlock
conceptually is a list containing objects with attribute or block names.
Given following schema example:
schema.Schema{
Blocks: map[string]schema.Block{
"root_list_block": schema.ListNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"block_string_attribute": schema.StringAttribute{
Required: true,
},
},
Blocks: map[string]schema.Block{
"nested_list_block": schema.ListNestedBlock{
Attributes: map[string]schema.Attribute{
"nested_block_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
},
},
},
},
}
The path which matches the list associated with the root_list_block
block is:
path.Root("root_list_block")
The path which matches the first object in the list associated with the root_list_block
block is:
path.Root("root_list_block").AtListIndex(0)
The path which matches the block_string_attribute
string value in the first object in the list associated with root_list_block
block is:
path.Root("root_list_block").AtListIndex(0).AtName("block_string_attribute")
The path which matches the nested_list_block
list in the first object in the list associated with root_list_block
block is:
path.Root("root_list_block").AtListIndex(0).AtName("nested_list_block")
The path which matches the nested_block_string_attribute
string value in the first object in the list associated with the nested_list_block
list in the first object in the list associated with root_list_block
block is:
path.Root("root_list_block").AtListIndex(0).AtName("nested_list_block").AtListIndex(0).AtName("nested_block_string_attribute")
Building Set Block Paths
A block defined with schema.SetNestedBlock
conceptually is a set containing objects with attribute or block names. Attempting to build set block paths is extremely tedius as set element selection is based on the entire value of the element, including any null or unknown values. Avoid manual set-based path building. Instead, use functionality which supports path expressions as that supports wildcard path matching (path.Expression
type AtAnySetValue()
method) or relative paths.
Given following schema example:
schema.Schema{
Blocks: map[string]schema.Block{
"root_set_block": schema.SetNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"block_string_attribute": schema.StringAttribute{
Optional: true,
},
},
},
},
},
}
The path which matches the set associated with the root_set_block
block is:
path.Root("root_set_block")
Examples below will presume a block_string_attribute
string value of types.StringValue("example")
for brevity. In real world usage, the string value may be types.StringNull()
, types.StringUnknown()
or types.StringValue("something-else")
, which are all considered different set paths from each other. Each additional attribute or block introduces exponentially more possible paths given each attribute or block value may be null, unknown, or a unique known value.
The path which matches the object associated with the root_set_block
block is:
path.Root("root_set_block").AtSetValue(types.ObjectValueMust(
map[string]attr.Type{
"block_string_attribute": types.StringType,
},
map[string]attr.Value{
"block_string_attribute": types.StringValue("example"),
}
))
The path which matches the block_string_attribute
string value associated with root_set_block
block is:
path.Root("root_set_block").AtSetValue(types.ObjectValueMust(
map[string]attr.Type{
"block_string_attribute": types.StringType,
},
map[string]attr.Value{
"block_string_attribute": types.StringValue("example"),
}
)).AtName("block_string_attribute")
Building Single Block Paths
A block defined with schema.SingleNestedBlock
conceptually is an object with attribute or block names.
Given following schema example:
schema.Schema{
Blocks: map[string]schema.Block{
"root_single_block": schema.SingleNestedBlock{
Attributes: map[string]schema.Attribute{
"block_string_attribute": schema.StringAttribute{
Required: true,
},
},
Blocks: map[string]schema.Block{
"nested_single_block": schema.SingleNestedBlock{
Attributes: map[string]schema.Attribute{
"nested_block_string_attribute": schema.StringAttribute{
Required: true,
},
},
},
},
},
},
}
The path which matches the object associated with the root_single_block
block is:
path.Root("root_single_block")
The path which matches the block_string_attribute
string value in the object associated with root_single_block
block is:
path.Root("root_single_block").AtName("block_string_attribute")
The path which matches the nested_single_block
object in the object associated with root_single_block
block is:
path.Root("root_single_block").AtName("nested_single_block")
The path which matches the nested_block_string_attribute
string value in the object associated with the nested_single_block
in the object associated with root_single_block
block is:
path.Root("root_single_block").AtName("nested_single_block").AtName("nested_block_string_attribute")
Building Dynamic Attribute Paths
An attribute that implements schema.DynamicAttribute
does not have a statically defined type, as the underlying value type is determined at runtime. When building paths for dynamic values, always target the root dynamic attribute.
Given the following schema example:
schema.Schema{
Attributes: map[string]schema.Attribute{
"root_dynamic_attribute": schema.DynamicAttribute{
Required: true,
},
},
}
The path which matches the dynamic value associated with the root_dynamic_attribute
attribute is:
path.Root("root_dynamic_attribute")