1package schema
2
3import (
4	"encoding/json"
5
6	"github.com/zclconf/go-cty/cty"
7	ctyjson "github.com/zclconf/go-cty/cty/json"
8
9	"github.com/hashicorp/terraform/config"
10	"github.com/hashicorp/terraform/configs/configschema"
11	"github.com/hashicorp/terraform/terraform"
12)
13
14// DiffFromValues takes the current state and desired state as cty.Values and
15// derives a terraform.InstanceDiff to give to the legacy providers. This is
16// used to take the states provided by the new ApplyResourceChange method and
17// convert them to a state+diff required for the legacy Apply method.
18func DiffFromValues(prior, planned cty.Value, res *Resource) (*terraform.InstanceDiff, error) {
19	return diffFromValues(prior, planned, res, nil)
20}
21
22// diffFromValues takes an additional CustomizeDiffFunc, so we can generate our
23// test fixtures from the legacy tests. In the new provider protocol the diff
24// only needs to be created for the apply operation, and any customizations
25// have already been done.
26func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffFunc) (*terraform.InstanceDiff, error) {
27	instanceState, err := res.ShimInstanceStateFromValue(prior)
28	if err != nil {
29		return nil, err
30	}
31
32	configSchema := res.CoreConfigSchema()
33
34	cfg := terraform.NewResourceConfigShimmed(planned, configSchema)
35	removeConfigUnknowns(cfg.Config)
36	removeConfigUnknowns(cfg.Raw)
37
38	diff, err := schemaMap(res.Schema).Diff(instanceState, cfg, cust, nil, false)
39	if err != nil {
40		return nil, err
41	}
42
43	return diff, err
44}
45
46// During apply the only unknown values are those which are to be computed by
47// the resource itself. These may have been marked as unknown config values, and
48// need to be removed to prevent the UnknownVariableValue from appearing the diff.
49func removeConfigUnknowns(cfg map[string]interface{}) {
50	for k, v := range cfg {
51		switch v := v.(type) {
52		case string:
53			if v == config.UnknownVariableValue {
54				delete(cfg, k)
55			}
56		case []interface{}:
57			for _, i := range v {
58				if m, ok := i.(map[string]interface{}); ok {
59					removeConfigUnknowns(m)
60				}
61			}
62		case map[string]interface{}:
63			removeConfigUnknowns(v)
64		}
65	}
66}
67
68// ApplyDiff takes a cty.Value state and applies a terraform.InstanceDiff to
69// get a new cty.Value state. This is used to convert the diff returned from
70// the legacy provider Diff method to the state required for the new
71// PlanResourceChange method.
72func ApplyDiff(base cty.Value, d *terraform.InstanceDiff, schema *configschema.Block) (cty.Value, error) {
73	return d.ApplyToValue(base, schema)
74}
75
76// StateValueToJSONMap converts a cty.Value to generic JSON map via the cty JSON
77// encoding.
78func StateValueToJSONMap(val cty.Value, ty cty.Type) (map[string]interface{}, error) {
79	js, err := ctyjson.Marshal(val, ty)
80	if err != nil {
81		return nil, err
82	}
83
84	var m map[string]interface{}
85	if err := json.Unmarshal(js, &m); err != nil {
86		return nil, err
87	}
88
89	return m, nil
90}
91
92// JSONMapToStateValue takes a generic json map[string]interface{} and converts it
93// to the specific type, ensuring that the values conform to the schema.
94func JSONMapToStateValue(m map[string]interface{}, block *configschema.Block) (cty.Value, error) {
95	var val cty.Value
96
97	js, err := json.Marshal(m)
98	if err != nil {
99		return val, err
100	}
101
102	val, err = ctyjson.Unmarshal(js, block.ImpliedType())
103	if err != nil {
104		return val, err
105	}
106
107	return block.CoerceValue(val)
108}
109
110// StateValueFromInstanceState converts a terraform.InstanceState to a
111// cty.Value as described by the provided cty.Type, and maintains the resource
112// ID as the "id" attribute.
113func StateValueFromInstanceState(is *terraform.InstanceState, ty cty.Type) (cty.Value, error) {
114	return is.AttrsAsObjectValue(ty)
115}
116