1package schema
2
3import (
4	"fmt"
5	"log"
6	"time"
7
8	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim"
9	"github.com/hashicorp/terraform-plugin-sdk/terraform"
10	"github.com/mitchellh/copystructure"
11)
12
13const TimeoutKey = "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0"
14const TimeoutsConfigKey = "timeouts"
15
16const (
17	TimeoutCreate  = "create"
18	TimeoutRead    = "read"
19	TimeoutUpdate  = "update"
20	TimeoutDelete  = "delete"
21	TimeoutDefault = "default"
22)
23
24func timeoutKeys() []string {
25	return []string{
26		TimeoutCreate,
27		TimeoutRead,
28		TimeoutUpdate,
29		TimeoutDelete,
30		TimeoutDefault,
31	}
32}
33
34// could be time.Duration, int64 or float64
35func DefaultTimeout(tx interface{}) *time.Duration {
36	var td time.Duration
37	switch raw := tx.(type) {
38	case time.Duration:
39		return &raw
40	case int64:
41		td = time.Duration(raw)
42	case float64:
43		td = time.Duration(int64(raw))
44	default:
45		log.Printf("[WARN] Unknown type in DefaultTimeout: %#v", tx)
46	}
47	return &td
48}
49
50type ResourceTimeout struct {
51	Create, Read, Update, Delete, Default *time.Duration
52}
53
54// ConfigDecode takes a schema and the configuration (available in Diff) and
55// validates, parses the timeouts into `t`
56func (t *ResourceTimeout) ConfigDecode(s *Resource, c *terraform.ResourceConfig) error {
57	if s.Timeouts != nil {
58		raw, err := copystructure.Copy(s.Timeouts)
59		if err != nil {
60			log.Printf("[DEBUG] Error with deep copy: %s", err)
61		}
62		*t = *raw.(*ResourceTimeout)
63	}
64
65	if raw, ok := c.Config[TimeoutsConfigKey]; ok {
66		var rawTimeouts []map[string]interface{}
67		switch raw := raw.(type) {
68		case map[string]interface{}:
69			rawTimeouts = append(rawTimeouts, raw)
70		case []map[string]interface{}:
71			rawTimeouts = raw
72		case string:
73			if raw == hcl2shim.UnknownVariableValue {
74				// Timeout is not defined in the config
75				// Defaults will be used instead
76				return nil
77			} else {
78				log.Printf("[ERROR] Invalid timeout value: %q", raw)
79				return fmt.Errorf("Invalid Timeout value found")
80			}
81		case []interface{}:
82			for _, r := range raw {
83				if rMap, ok := r.(map[string]interface{}); ok {
84					rawTimeouts = append(rawTimeouts, rMap)
85				} else {
86					// Go will not allow a fallthrough
87					log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
88					return fmt.Errorf("Invalid Timeout structure found")
89				}
90			}
91		default:
92			log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
93			return fmt.Errorf("Invalid Timeout structure found")
94		}
95
96		for _, timeoutValues := range rawTimeouts {
97			for timeKey, timeValue := range timeoutValues {
98				// validate that we're dealing with the normal CRUD actions
99				var found bool
100				for _, key := range timeoutKeys() {
101					if timeKey == key {
102						found = true
103						break
104					}
105				}
106
107				if !found {
108					return fmt.Errorf("Unsupported Timeout configuration key found (%s)", timeKey)
109				}
110
111				// Get timeout
112				rt, err := time.ParseDuration(timeValue.(string))
113				if err != nil {
114					return fmt.Errorf("Error parsing %q timeout: %s", timeKey, err)
115				}
116
117				var timeout *time.Duration
118				switch timeKey {
119				case TimeoutCreate:
120					timeout = t.Create
121				case TimeoutUpdate:
122					timeout = t.Update
123				case TimeoutRead:
124					timeout = t.Read
125				case TimeoutDelete:
126					timeout = t.Delete
127				case TimeoutDefault:
128					timeout = t.Default
129				}
130
131				// If the resource has not delcared this in the definition, then error
132				// with an unsupported message
133				if timeout == nil {
134					return unsupportedTimeoutKeyError(timeKey)
135				}
136
137				*timeout = rt
138			}
139			return nil
140		}
141	}
142
143	return nil
144}
145
146func unsupportedTimeoutKeyError(key string) error {
147	return fmt.Errorf("Timeout Key (%s) is not supported", key)
148}
149
150// DiffEncode, StateEncode, and MetaDecode are analogous to the Go stdlib JSONEncoder
151// interface: they encode/decode a timeouts struct from an instance diff, which is
152// where the timeout data is stored after a diff to pass into Apply.
153//
154// StateEncode encodes the timeout into the ResourceData's InstanceState for
155// saving to state
156//
157func (t *ResourceTimeout) DiffEncode(id *terraform.InstanceDiff) error {
158	return t.metaEncode(id)
159}
160
161func (t *ResourceTimeout) StateEncode(is *terraform.InstanceState) error {
162	return t.metaEncode(is)
163}
164
165// metaEncode encodes the ResourceTimeout into a map[string]interface{} format
166// and stores it in the Meta field of the interface it's given.
167// Assumes the interface is either *terraform.InstanceState or
168// *terraform.InstanceDiff, returns an error otherwise
169func (t *ResourceTimeout) metaEncode(ids interface{}) error {
170	m := make(map[string]interface{})
171
172	if t.Create != nil {
173		m[TimeoutCreate] = t.Create.Nanoseconds()
174	}
175	if t.Read != nil {
176		m[TimeoutRead] = t.Read.Nanoseconds()
177	}
178	if t.Update != nil {
179		m[TimeoutUpdate] = t.Update.Nanoseconds()
180	}
181	if t.Delete != nil {
182		m[TimeoutDelete] = t.Delete.Nanoseconds()
183	}
184	if t.Default != nil {
185		m[TimeoutDefault] = t.Default.Nanoseconds()
186		// for any key above that is nil, if default is specified, we need to
187		// populate it with the default
188		for _, k := range timeoutKeys() {
189			if _, ok := m[k]; !ok {
190				m[k] = t.Default.Nanoseconds()
191			}
192		}
193	}
194
195	// only add the Timeout to the Meta if we have values
196	if len(m) > 0 {
197		switch instance := ids.(type) {
198		case *terraform.InstanceDiff:
199			if instance.Meta == nil {
200				instance.Meta = make(map[string]interface{})
201			}
202			instance.Meta[TimeoutKey] = m
203		case *terraform.InstanceState:
204			if instance.Meta == nil {
205				instance.Meta = make(map[string]interface{})
206			}
207			instance.Meta[TimeoutKey] = m
208		default:
209			return fmt.Errorf("Error matching type for Diff Encode")
210		}
211	}
212
213	return nil
214}
215
216func (t *ResourceTimeout) StateDecode(id *terraform.InstanceState) error {
217	return t.metaDecode(id)
218}
219func (t *ResourceTimeout) DiffDecode(is *terraform.InstanceDiff) error {
220	return t.metaDecode(is)
221}
222
223func (t *ResourceTimeout) metaDecode(ids interface{}) error {
224	var rawMeta interface{}
225	var ok bool
226	switch rawInstance := ids.(type) {
227	case *terraform.InstanceDiff:
228		rawMeta, ok = rawInstance.Meta[TimeoutKey]
229		if !ok {
230			return nil
231		}
232	case *terraform.InstanceState:
233		rawMeta, ok = rawInstance.Meta[TimeoutKey]
234		if !ok {
235			return nil
236		}
237	default:
238		return fmt.Errorf("Unknown or unsupported type in metaDecode: %#v", ids)
239	}
240
241	times := rawMeta.(map[string]interface{})
242	if len(times) == 0 {
243		return nil
244	}
245
246	if v, ok := times[TimeoutCreate]; ok {
247		t.Create = DefaultTimeout(v)
248	}
249	if v, ok := times[TimeoutRead]; ok {
250		t.Read = DefaultTimeout(v)
251	}
252	if v, ok := times[TimeoutUpdate]; ok {
253		t.Update = DefaultTimeout(v)
254	}
255	if v, ok := times[TimeoutDelete]; ok {
256		t.Delete = DefaultTimeout(v)
257	}
258	if v, ok := times[TimeoutDefault]; ok {
259		t.Default = DefaultTimeout(v)
260	}
261
262	return nil
263}
264