1package schema
2
3import (
4	"fmt"
5	"log"
6	"time"
7
8	"github.com/hashicorp/terraform/config"
9	"github.com/hashicorp/terraform/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 == config.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		default:
82			log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
83			return fmt.Errorf("Invalid Timeout structure found")
84		}
85
86		for _, timeoutValues := range rawTimeouts {
87			for timeKey, timeValue := range timeoutValues {
88				// validate that we're dealing with the normal CRUD actions
89				var found bool
90				for _, key := range timeoutKeys() {
91					if timeKey == key {
92						found = true
93						break
94					}
95				}
96
97				if !found {
98					return fmt.Errorf("Unsupported Timeout configuration key found (%s)", timeKey)
99				}
100
101				// Get timeout
102				rt, err := time.ParseDuration(timeValue.(string))
103				if err != nil {
104					return fmt.Errorf("Error parsing %q timeout: %s", timeKey, err)
105				}
106
107				var timeout *time.Duration
108				switch timeKey {
109				case TimeoutCreate:
110					timeout = t.Create
111				case TimeoutUpdate:
112					timeout = t.Update
113				case TimeoutRead:
114					timeout = t.Read
115				case TimeoutDelete:
116					timeout = t.Delete
117				case TimeoutDefault:
118					timeout = t.Default
119				}
120
121				// If the resource has not delcared this in the definition, then error
122				// with an unsupported message
123				if timeout == nil {
124					return unsupportedTimeoutKeyError(timeKey)
125				}
126
127				*timeout = rt
128			}
129			return nil
130		}
131	}
132
133	return nil
134}
135
136func unsupportedTimeoutKeyError(key string) error {
137	return fmt.Errorf("Timeout Key (%s) is not supported", key)
138}
139
140// DiffEncode, StateEncode, and MetaDecode are analogous to the Go stdlib JSONEncoder
141// interface: they encode/decode a timeouts struct from an instance diff, which is
142// where the timeout data is stored after a diff to pass into Apply.
143//
144// StateEncode encodes the timeout into the ResourceData's InstanceState for
145// saving to state
146//
147func (t *ResourceTimeout) DiffEncode(id *terraform.InstanceDiff) error {
148	return t.metaEncode(id)
149}
150
151func (t *ResourceTimeout) StateEncode(is *terraform.InstanceState) error {
152	return t.metaEncode(is)
153}
154
155// metaEncode encodes the ResourceTimeout into a map[string]interface{} format
156// and stores it in the Meta field of the interface it's given.
157// Assumes the interface is either *terraform.InstanceState or
158// *terraform.InstanceDiff, returns an error otherwise
159func (t *ResourceTimeout) metaEncode(ids interface{}) error {
160	m := make(map[string]interface{})
161
162	if t.Create != nil {
163		m[TimeoutCreate] = t.Create.Nanoseconds()
164	}
165	if t.Read != nil {
166		m[TimeoutRead] = t.Read.Nanoseconds()
167	}
168	if t.Update != nil {
169		m[TimeoutUpdate] = t.Update.Nanoseconds()
170	}
171	if t.Delete != nil {
172		m[TimeoutDelete] = t.Delete.Nanoseconds()
173	}
174	if t.Default != nil {
175		m[TimeoutDefault] = t.Default.Nanoseconds()
176		// for any key above that is nil, if default is specified, we need to
177		// populate it with the default
178		for _, k := range timeoutKeys() {
179			if _, ok := m[k]; !ok {
180				m[k] = t.Default.Nanoseconds()
181			}
182		}
183	}
184
185	// only add the Timeout to the Meta if we have values
186	if len(m) > 0 {
187		switch instance := ids.(type) {
188		case *terraform.InstanceDiff:
189			if instance.Meta == nil {
190				instance.Meta = make(map[string]interface{})
191			}
192			instance.Meta[TimeoutKey] = m
193		case *terraform.InstanceState:
194			if instance.Meta == nil {
195				instance.Meta = make(map[string]interface{})
196			}
197			instance.Meta[TimeoutKey] = m
198		default:
199			return fmt.Errorf("Error matching type for Diff Encode")
200		}
201	}
202
203	return nil
204}
205
206func (t *ResourceTimeout) StateDecode(id *terraform.InstanceState) error {
207	return t.metaDecode(id)
208}
209func (t *ResourceTimeout) DiffDecode(is *terraform.InstanceDiff) error {
210	return t.metaDecode(is)
211}
212
213func (t *ResourceTimeout) metaDecode(ids interface{}) error {
214	var rawMeta interface{}
215	var ok bool
216	switch rawInstance := ids.(type) {
217	case *terraform.InstanceDiff:
218		rawMeta, ok = rawInstance.Meta[TimeoutKey]
219		if !ok {
220			return nil
221		}
222	case *terraform.InstanceState:
223		rawMeta, ok = rawInstance.Meta[TimeoutKey]
224		if !ok {
225			return nil
226		}
227	default:
228		return fmt.Errorf("Unknown or unsupported type in metaDecode: %#v", ids)
229	}
230
231	times := rawMeta.(map[string]interface{})
232	if len(times) == 0 {
233		return nil
234	}
235
236	if v, ok := times[TimeoutCreate]; ok {
237		t.Create = DefaultTimeout(v)
238	}
239	if v, ok := times[TimeoutRead]; ok {
240		t.Read = DefaultTimeout(v)
241	}
242	if v, ok := times[TimeoutUpdate]; ok {
243		t.Update = DefaultTimeout(v)
244	}
245	if v, ok := times[TimeoutDelete]; ok {
246		t.Delete = DefaultTimeout(v)
247	}
248	if v, ok := times[TimeoutDefault]; ok {
249		t.Default = DefaultTimeout(v)
250	}
251
252	return nil
253}
254