Writing State
One of the primary jobs of a Terraform provider is to manage the provider's resources and data sources in the Terraform statefile. Writing values to state is something that provider developers will do frequently.
The state that a provider developer wants to update is usually stored in a response object:
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse)
In this example, resp
holds the state that the provider developer should
update.
Replace the Entire State
One way to set the state is to replace all the state values for a resource or data source all at once. You need to define a type to contain the values. The benefit is that this allows the compiler to check all code that sets values on state, and only the final call to persist state can return an error.
type resourceData struct {
Name types.Strings `tfsdk:"name"`
Age types.Number `tfsdk:"age"`
Registered types.Bool `tfsdk:"registered"`
Pets types.List `tfsdk:"pets"`
Tags types.Map `tfsdk:"tags"`
Address types.Object `tfsdk:"address"`
}
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
var newState resourceData
// update newState by modifying each property as usual for Go values
newState.Name.Value = "J. Doe"
// persist the values to state
diags := resp.State.Set(ctx, &newState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
The state information is represented as an object, and gets persisted like an object. See the conversion rules for an explanation on how objects get persisted and what Go types are valid for persisting as an object.
Using the attr.Value
implementations can
surface complexity that is unnecessary, however. For example, you can never set
an unknown value in state, so there's no need to be able to express unknown
values when setting state.
To make things a little easier, and to ensure that any type that
Get
can convert into can also be used as a value for Set
, the framework can do
some conversion on values passed to Set
:
type resourceData struct {
Name string `tfsdk:"name"`
Age int64 `tfsdk:"age"`
Registered bool `tfsdk:"registered"`
Pets []string `tfsdk:"pets"`
Tags map[string]string `tfsdk:"tags"`
Address struct{
Street string `tfsdk:"street"`
City string `tfsdk:"city"`
State string `tfsdk:"state"`
Zip int64 `tfsdk:"zip"`
} `tfsdk:"address"`
}
See below for the rules about conversion.
Set a Single Attribute's Value
Another way to set values in the state is to write each new value separately. This doesn't require defining a type (except for objects), but means each value update steps outside of what the compiler can check, and may return an error at runtime.
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
age := types.Number{Value: big.NewFloat(7)}
diags := resp.State.SetAttribute(ctx, path.Root("age"), &age)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
Like Set
, SetAttribute
can also do some conversion to standard Go types:
func (m myResource) Create(ctx context.Context,
req resource.CreateRequest, resp *resource.CreateResponse) {
var age int64
age = 7
diags := resp.State.SetAttribute(ctx, path.Root("age"), &age)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
See below for the rules about conversion.
Conversion Rules
The following is a list of schema types and the Go types they know how to
accept in Set
and SetAttribute
.
String
Strings can be automatically created from Go's string
type (or any aliases of
it, like type MyString string
).
Number
Numbers can be automatically created from the following numeric types (or any
aliases of them, like type MyNumber int
):
int
,int8
,int16
,int32
,int64
uint
,uint8
,uint16
,uint32
,uint64
float32
,float64
*big.Int
,*big.Float
Boolean
Booleans can be automatically created from Go's bool
type (or any aliases of
it, like type MyBoolean bool
).
List
Lists can be automatically created from any Go slice type (or alias of a Go
slice type, like type MyList []string
), with the elements either being
attr.Value
implementations or being converted according to these rules.
Map
Maps can be automatically created from any Go map type with string keys (or any
alias of a Go map type with string keys, like type MyMap map[string]int
),
with the elements either being attr.Value
implementations or being converted
according to these rules.
Object
Objects can be automatically created from any Go struct type with that follows these constraints:
- Every property on the struct must have a
tfsdk
struct tag. - The
tfsdk
struct tag must name an attribute in the object that it is being mapped to or be set to-
to explicitly declare it does not map to an attribute in the object. - Every attribute in the object must have a corresponding struct tag.
These rules help prevent typos and human error from unwittingly discarding information by failing as early, consistently, and loudly as possible.
Properties can either be attr.Value
implementations or will be converted
according to these rules.
Pointers
A nil pointer will be treated as a null value. Otherwise, the rules for the type the pointer is referencing apply.
Detected Interfaces
Set
detects and utilizes the following interfaces, if the target implements
them.
ValueCreator
If a value is set on a Go type that implements the tftypes.ValueCreator
interface,
that interface will be delegated to to handle the conversion.
Unknownable
If a value is set on a Go type that fills the Unknownable
interface:
type Unknownable interface {
SetUnknown(context.Context, bool) error
SetValue(context.Context, interface{}) error
GetUnknown(context.Context) bool
GetValue(context.Context) interface{}
}
It will be used to convert the value. The interface{}
being passed and
retrieved will be of a type that can be passed to
tftypes.NewValue
.
Nullable
If a value is set on a Go type that fills the Nullable
interface:
type Nullable interface {
SetNull(context.Context, bool) error
SetValue(context.Context, interface{}) error
GetNull(context.Context) bool
GetValue(context.Context) interface{}
}
It will be used to convert the value. The interface{}
being passed and
retrieved will be of a type that can be passed to
tftypes.NewValue
.