mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-24 15:36:26 -06:00
c7550595a3
This was actually quite nasty as the first bug covered the second one… The first bug is with HasChange. This function uses reflect.DeepEqual to check if two instances are the same/have the same content. This works fine for all types except for Set’s as they contain a function. And reflect.DeepEqual will only say the functions are equal if they are both nil (which they aren’t in a Set). So in effect it means that currently HasChange will always say true for Set’s, even when they are actually being equal. As soon as you fix this problem, you will notice the second one (which the added test is written for). Without saying you want the exact diff, you will end up with a merged value which will (in most cases) be the same. Run all unit tests and a good part of the acc tests to verify this works as expected and all look good.
394 lines
10 KiB
Go
394 lines
10 KiB
Go
package schema
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
// ResourceData is used to query and set the attributes of a resource.
|
|
//
|
|
// ResourceData is the primary argument received for CRUD operations on
|
|
// a resource as well as configuration of a provider. It is a powerful
|
|
// structure that can be used to not only query data, but check for changes,
|
|
// define partial state updates, etc.
|
|
//
|
|
// The most relevant methods to take a look at are Get, Set, and Partial.
|
|
type ResourceData struct {
|
|
// Settable (internally)
|
|
schema map[string]*Schema
|
|
config *terraform.ResourceConfig
|
|
state *terraform.InstanceState
|
|
diff *terraform.InstanceDiff
|
|
|
|
// Don't set
|
|
multiReader *MultiLevelFieldReader
|
|
setWriter *MapFieldWriter
|
|
newState *terraform.InstanceState
|
|
partial bool
|
|
partialMap map[string]struct{}
|
|
once sync.Once
|
|
}
|
|
|
|
// getSource represents the level we want to get for a value (internally).
|
|
// Any source less than or equal to the level will be loaded (whichever
|
|
// has a value first).
|
|
type getSource byte
|
|
|
|
const (
|
|
getSourceState getSource = 1 << iota
|
|
getSourceConfig
|
|
getSourceDiff
|
|
getSourceSet
|
|
getSourceExact // Only get from the _exact_ level
|
|
getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceDiff | getSourceSet
|
|
)
|
|
|
|
// getResult is the internal structure that is generated when a Get
|
|
// is called that contains some extra data that might be used.
|
|
type getResult struct {
|
|
Value interface{}
|
|
ValueProcessed interface{}
|
|
Computed bool
|
|
Exists bool
|
|
Schema *Schema
|
|
}
|
|
|
|
var getResultEmpty getResult
|
|
|
|
// Get returns the data for the given key, or nil if the key doesn't exist
|
|
// in the schema.
|
|
//
|
|
// If the key does exist in the schema but doesn't exist in the configuration,
|
|
// then the default value for that type will be returned. For strings, this is
|
|
// "", for numbers it is 0, etc.
|
|
//
|
|
// If you want to test if something is set at all in the configuration,
|
|
// use GetOk.
|
|
func (d *ResourceData) Get(key string) interface{} {
|
|
v, _ := d.GetOk(key)
|
|
return v
|
|
}
|
|
|
|
// GetChange returns the old and new value for a given key.
|
|
//
|
|
// HasChange should be used to check if a change exists. It is possible
|
|
// that both the old and new value are the same if the old value was not
|
|
// set and the new value is. This is common, for example, for boolean
|
|
// fields which have a zero value of false.
|
|
func (d *ResourceData) GetChange(key string) (interface{}, interface{}) {
|
|
o, n := d.getChange(key, getSourceState, getSourceDiff|getSourceExact)
|
|
return o.Value, n.Value
|
|
}
|
|
|
|
// GetOk returns the data for the given key and whether or not the key
|
|
// has been set.
|
|
//
|
|
// The first result will not necessarilly be nil if the value doesn't exist.
|
|
// The second result should be checked to determine this information.
|
|
func (d *ResourceData) GetOk(key string) (interface{}, bool) {
|
|
r := d.getRaw(key, getSourceSet)
|
|
return r.Value, r.Exists && !r.Computed
|
|
}
|
|
|
|
func (d *ResourceData) getRaw(key string, level getSource) getResult {
|
|
var parts []string
|
|
if key != "" {
|
|
parts = strings.Split(key, ".")
|
|
}
|
|
|
|
return d.get(parts, level)
|
|
}
|
|
|
|
// HasChange returns whether or not the given key has been changed.
|
|
func (d *ResourceData) HasChange(key string) bool {
|
|
o, n := d.GetChange(key)
|
|
|
|
// There is a special case needed for *schema.Set's as they contain
|
|
// a function and reflect.DeepEqual will only say two functions are
|
|
// equal when they are both nil (which in this case they are not).
|
|
if reflect.TypeOf(o).String() == "*schema.Set" {
|
|
return !reflect.DeepEqual(o.(*Set).m, n.(*Set).m)
|
|
}
|
|
|
|
return !reflect.DeepEqual(o, n)
|
|
}
|
|
|
|
// Partial turns partial state mode on/off.
|
|
//
|
|
// When partial state mode is enabled, then only key prefixes specified
|
|
// by SetPartial will be in the final state. This allows providers to return
|
|
// partial states for partially applied resources (when errors occur).
|
|
func (d *ResourceData) Partial(on bool) {
|
|
d.partial = on
|
|
if on {
|
|
if d.partialMap == nil {
|
|
d.partialMap = make(map[string]struct{})
|
|
}
|
|
} else {
|
|
d.partialMap = nil
|
|
}
|
|
}
|
|
|
|
// Set sets the value for the given key.
|
|
//
|
|
// If the key is invalid or the value is not a correct type, an error
|
|
// will be returned.
|
|
func (d *ResourceData) Set(key string, value interface{}) error {
|
|
d.once.Do(d.init)
|
|
return d.setWriter.WriteField(strings.Split(key, "."), value)
|
|
}
|
|
|
|
// SetPartial adds the key to the final state output while
|
|
// in partial state mode. The key must be a root key in the schema (i.e.
|
|
// it cannot be "list.0").
|
|
//
|
|
// If partial state mode is disabled, then this has no effect. Additionally,
|
|
// whenever partial state mode is toggled, the partial data is cleared.
|
|
func (d *ResourceData) SetPartial(k string) {
|
|
if d.partial {
|
|
d.partialMap[k] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Id returns the ID of the resource.
|
|
func (d *ResourceData) Id() string {
|
|
var result string
|
|
|
|
if d.state != nil {
|
|
result = d.state.ID
|
|
}
|
|
|
|
if d.newState != nil {
|
|
result = d.newState.ID
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ConnInfo returns the connection info for this resource.
|
|
func (d *ResourceData) ConnInfo() map[string]string {
|
|
if d.newState != nil {
|
|
return d.newState.Ephemeral.ConnInfo
|
|
}
|
|
|
|
if d.state != nil {
|
|
return d.state.Ephemeral.ConnInfo
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetId sets the ID of the resource. If the value is blank, then the
|
|
// resource is destroyed.
|
|
func (d *ResourceData) SetId(v string) {
|
|
d.once.Do(d.init)
|
|
d.newState.ID = v
|
|
}
|
|
|
|
// SetConnInfo sets the connection info for a resource.
|
|
func (d *ResourceData) SetConnInfo(v map[string]string) {
|
|
d.once.Do(d.init)
|
|
d.newState.Ephemeral.ConnInfo = v
|
|
}
|
|
|
|
// State returns the new InstanceState after the diff and any Set
|
|
// calls.
|
|
func (d *ResourceData) State() *terraform.InstanceState {
|
|
var result terraform.InstanceState
|
|
result.ID = d.Id()
|
|
|
|
// If we have no ID, then this resource doesn't exist and we just
|
|
// return nil.
|
|
if result.ID == "" {
|
|
return nil
|
|
}
|
|
|
|
// In order to build the final state attributes, we read the full
|
|
// attribute set as a map[string]interface{}, write it to a MapFieldWriter,
|
|
// and then use that map.
|
|
rawMap := make(map[string]interface{})
|
|
for k, _ := range d.schema {
|
|
source := getSourceSet
|
|
if d.partial {
|
|
source = getSourceState
|
|
if _, ok := d.partialMap[k]; ok {
|
|
source = getSourceSet
|
|
}
|
|
}
|
|
|
|
raw := d.get([]string{k}, source)
|
|
if raw.Exists && !raw.Computed {
|
|
rawMap[k] = raw.Value
|
|
if raw.ValueProcessed != nil {
|
|
rawMap[k] = raw.ValueProcessed
|
|
}
|
|
}
|
|
}
|
|
mapW := &MapFieldWriter{Schema: d.schema}
|
|
if err := mapW.WriteField(nil, rawMap); err != nil {
|
|
return nil
|
|
}
|
|
|
|
result.Attributes = mapW.Map()
|
|
result.Ephemeral.ConnInfo = d.ConnInfo()
|
|
|
|
// TODO: This is hacky and we can remove this when we have a proper
|
|
// state writer. We should instead have a proper StateFieldWriter
|
|
// and use that.
|
|
for k, schema := range d.schema {
|
|
if schema.Type != TypeMap {
|
|
continue
|
|
}
|
|
|
|
if result.Attributes[k] == "" {
|
|
delete(result.Attributes, k)
|
|
}
|
|
}
|
|
|
|
if v := d.Id(); v != "" {
|
|
result.Attributes["id"] = d.Id()
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
func (d *ResourceData) init() {
|
|
// Initialize the field that will store our new state
|
|
var copyState terraform.InstanceState
|
|
if d.state != nil {
|
|
copyState = *d.state
|
|
}
|
|
d.newState = ©State
|
|
|
|
// Initialize the map for storing set data
|
|
d.setWriter = &MapFieldWriter{Schema: d.schema}
|
|
|
|
// Initialize the reader for getting data from the
|
|
// underlying sources (config, diff, etc.)
|
|
readers := make(map[string]FieldReader)
|
|
var stateAttributes map[string]string
|
|
if d.state != nil {
|
|
stateAttributes = d.state.Attributes
|
|
readers["state"] = &MapFieldReader{
|
|
Schema: d.schema,
|
|
Map: BasicMapReader(stateAttributes),
|
|
}
|
|
}
|
|
if d.config != nil {
|
|
readers["config"] = &ConfigFieldReader{
|
|
Schema: d.schema,
|
|
Config: d.config,
|
|
}
|
|
}
|
|
if d.diff != nil {
|
|
readers["diff"] = &DiffFieldReader{
|
|
Schema: d.schema,
|
|
Diff: d.diff,
|
|
Source: &MultiLevelFieldReader{
|
|
Levels: []string{"state", "config"},
|
|
Readers: readers,
|
|
},
|
|
}
|
|
}
|
|
readers["set"] = &MapFieldReader{
|
|
Schema: d.schema,
|
|
Map: BasicMapReader(d.setWriter.Map()),
|
|
}
|
|
d.multiReader = &MultiLevelFieldReader{
|
|
Levels: []string{
|
|
"state",
|
|
"config",
|
|
"diff",
|
|
"set",
|
|
},
|
|
|
|
Readers: readers,
|
|
}
|
|
}
|
|
|
|
func (d *ResourceData) diffChange(
|
|
k string) (interface{}, interface{}, bool, bool) {
|
|
// Get the change between the state and the config.
|
|
o, n := d.getChange(k, getSourceState, getSourceConfig|getSourceExact)
|
|
if !o.Exists {
|
|
o.Value = nil
|
|
}
|
|
if !n.Exists {
|
|
n.Value = nil
|
|
}
|
|
|
|
// Return the old, new, and whether there is a change
|
|
return o.Value, n.Value, !reflect.DeepEqual(o.Value, n.Value), n.Computed
|
|
}
|
|
|
|
func (d *ResourceData) getChange(
|
|
key string,
|
|
oldLevel getSource,
|
|
newLevel getSource) (getResult, getResult) {
|
|
var parts, parts2 []string
|
|
if key != "" {
|
|
parts = strings.Split(key, ".")
|
|
parts2 = strings.Split(key, ".")
|
|
}
|
|
|
|
o := d.get(parts, oldLevel)
|
|
n := d.get(parts2, newLevel)
|
|
return o, n
|
|
}
|
|
|
|
func (d *ResourceData) get(addr []string, source getSource) getResult {
|
|
d.once.Do(d.init)
|
|
|
|
level := "set"
|
|
flags := source & ^getSourceLevelMask
|
|
exact := flags&getSourceExact != 0
|
|
source = source & getSourceLevelMask
|
|
if source >= getSourceSet {
|
|
level = "set"
|
|
} else if source >= getSourceDiff {
|
|
level = "diff"
|
|
} else if source >= getSourceConfig {
|
|
level = "config"
|
|
} else {
|
|
level = "state"
|
|
}
|
|
|
|
// Build the address of the key we're looking for and ask the FieldReader
|
|
for i, v := range addr {
|
|
if v[0] == '~' {
|
|
addr[i] = v[1:]
|
|
}
|
|
}
|
|
|
|
var result FieldReadResult
|
|
var err error
|
|
if exact {
|
|
result, err = d.multiReader.ReadFieldExact(addr, level)
|
|
} else {
|
|
result, err = d.multiReader.ReadFieldMerge(addr, level)
|
|
}
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// If the result doesn't exist, then we set the value to the zero value
|
|
if result.Value == nil {
|
|
if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 {
|
|
schema := schemaL[len(schemaL)-1]
|
|
result.Value = result.ValueOrZero(schema)
|
|
}
|
|
}
|
|
|
|
// Transform the FieldReadResult into a getResult. It might be worth
|
|
// merging these two structures one day.
|
|
return getResult{
|
|
Value: result.Value,
|
|
ValueProcessed: result.ValueProcessed,
|
|
Computed: result.Computed,
|
|
Exists: result.Exists,
|
|
}
|
|
}
|