mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
Due to some past confusion about the purpose of this package, it has grown to include a confusing mix of currently-viable code and legacy support code from the move to HCL 2. This has in turn caused confusion about which parts of this package _should_ be used for new code. To help clarify that distinction we'll move the legacy support code into a package under the "legacy" directory, which is also where most of its callers live. There are unfortunately still some callers to these outside of the legacy tree, but the vast majority are either old tests written before HCL 2 adoption or helper code used only by those tests. The one dubious exception is the use in ResourceInstanceObjectSrc.Decode, which makes a best effort to shim flatmap as a concession to the fact that not all state-loading codepaths are able to run the provider state upgrade function that would normally be responsible for the flatmap-to-JSON conversion, which is explained in a new comment inline. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
268 lines
6.6 KiB
Go
268 lines
6.6 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package schema
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/opentofu/opentofu/internal/legacy/hcl2shim"
|
|
"github.com/opentofu/opentofu/internal/legacy/tofu"
|
|
)
|
|
|
|
const TimeoutKey = "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0"
|
|
const TimeoutsConfigKey = "timeouts"
|
|
|
|
const (
|
|
TimeoutCreate = "create"
|
|
TimeoutRead = "read"
|
|
TimeoutUpdate = "update"
|
|
TimeoutDelete = "delete"
|
|
TimeoutDefault = "default"
|
|
)
|
|
|
|
func timeoutKeys() []string {
|
|
return []string{
|
|
TimeoutCreate,
|
|
TimeoutRead,
|
|
TimeoutUpdate,
|
|
TimeoutDelete,
|
|
TimeoutDefault,
|
|
}
|
|
}
|
|
|
|
// could be time.Duration, int64 or float64
|
|
func DefaultTimeout(tx interface{}) *time.Duration {
|
|
var td time.Duration
|
|
switch raw := tx.(type) {
|
|
case time.Duration:
|
|
return &raw
|
|
case int64:
|
|
td = time.Duration(raw)
|
|
case float64:
|
|
td = time.Duration(int64(raw))
|
|
default:
|
|
log.Printf("[WARN] Unknown type in DefaultTimeout: %#v", tx)
|
|
}
|
|
return &td
|
|
}
|
|
|
|
type ResourceTimeout struct {
|
|
Create, Read, Update, Delete, Default *time.Duration
|
|
}
|
|
|
|
// ConfigDecode takes a schema and the configuration (available in Diff) and
|
|
// validates, parses the timeouts into `t`
|
|
func (t *ResourceTimeout) ConfigDecode(s *Resource, c *tofu.ResourceConfig) error {
|
|
if s.Timeouts != nil {
|
|
raw, err := copystructure.Copy(s.Timeouts)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] Error with deep copy: %s", err)
|
|
}
|
|
*t = *raw.(*ResourceTimeout)
|
|
}
|
|
|
|
if raw, ok := c.Config[TimeoutsConfigKey]; ok {
|
|
var rawTimeouts []map[string]interface{}
|
|
switch raw := raw.(type) {
|
|
case map[string]interface{}:
|
|
rawTimeouts = append(rawTimeouts, raw)
|
|
case []map[string]interface{}:
|
|
rawTimeouts = raw
|
|
case string:
|
|
if raw == hcl2shim.UnknownVariableValue {
|
|
// Timeout is not defined in the config
|
|
// Defaults will be used instead
|
|
return nil
|
|
} else {
|
|
log.Printf("[ERROR] Invalid timeout value: %q", raw)
|
|
return fmt.Errorf("Invalid Timeout value found")
|
|
}
|
|
case []interface{}:
|
|
for _, r := range raw {
|
|
if rMap, ok := r.(map[string]interface{}); ok {
|
|
rawTimeouts = append(rawTimeouts, rMap)
|
|
} else {
|
|
// Go will not allow a fallthrough
|
|
log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
|
|
return fmt.Errorf("Invalid Timeout structure found")
|
|
}
|
|
}
|
|
default:
|
|
log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
|
|
return fmt.Errorf("Invalid Timeout structure found")
|
|
}
|
|
|
|
for _, timeoutValues := range rawTimeouts {
|
|
for timeKey, timeValue := range timeoutValues {
|
|
// validate that we're dealing with the normal CRUD actions
|
|
var found bool
|
|
for _, key := range timeoutKeys() {
|
|
if timeKey == key {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("Unsupported Timeout configuration key found (%s)", timeKey)
|
|
}
|
|
|
|
// Get timeout
|
|
rt, err := time.ParseDuration(timeValue.(string))
|
|
if err != nil {
|
|
return fmt.Errorf("Error parsing %q timeout: %w", timeKey, err)
|
|
}
|
|
|
|
var timeout *time.Duration
|
|
switch timeKey {
|
|
case TimeoutCreate:
|
|
timeout = t.Create
|
|
case TimeoutUpdate:
|
|
timeout = t.Update
|
|
case TimeoutRead:
|
|
timeout = t.Read
|
|
case TimeoutDelete:
|
|
timeout = t.Delete
|
|
case TimeoutDefault:
|
|
timeout = t.Default
|
|
}
|
|
|
|
// If the resource has not declared this in the definition, then error
|
|
// with an unsupported message
|
|
if timeout == nil {
|
|
return unsupportedTimeoutKeyError(timeKey)
|
|
}
|
|
|
|
*timeout = rt
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func unsupportedTimeoutKeyError(key string) error {
|
|
return fmt.Errorf("Timeout Key (%s) is not supported", key)
|
|
}
|
|
|
|
// DiffEncode, StateEncode, and MetaDecode are analogous to the Go stdlib JSONEncoder
|
|
// interface: they encode/decode a timeouts struct from an instance diff, which is
|
|
// where the timeout data is stored after a diff to pass into Apply.
|
|
//
|
|
// StateEncode encodes the timeout into the ResourceData's InstanceState for
|
|
// saving to state
|
|
func (t *ResourceTimeout) DiffEncode(id *tofu.InstanceDiff) error {
|
|
return t.metaEncode(id)
|
|
}
|
|
|
|
func (t *ResourceTimeout) StateEncode(is *tofu.InstanceState) error {
|
|
return t.metaEncode(is)
|
|
}
|
|
|
|
// metaEncode encodes the ResourceTimeout into a map[string]interface{} format
|
|
// and stores it in the Meta field of the interface it's given.
|
|
// Assumes the interface is either *tofu.InstanceState or
|
|
// *tofu.InstanceDiff, returns an error otherwise
|
|
func (t *ResourceTimeout) metaEncode(ids interface{}) error {
|
|
m := make(map[string]interface{})
|
|
|
|
if t.Create != nil {
|
|
m[TimeoutCreate] = t.Create.Nanoseconds()
|
|
}
|
|
if t.Read != nil {
|
|
m[TimeoutRead] = t.Read.Nanoseconds()
|
|
}
|
|
if t.Update != nil {
|
|
m[TimeoutUpdate] = t.Update.Nanoseconds()
|
|
}
|
|
if t.Delete != nil {
|
|
m[TimeoutDelete] = t.Delete.Nanoseconds()
|
|
}
|
|
if t.Default != nil {
|
|
m[TimeoutDefault] = t.Default.Nanoseconds()
|
|
// for any key above that is nil, if default is specified, we need to
|
|
// populate it with the default
|
|
for _, k := range timeoutKeys() {
|
|
if _, ok := m[k]; !ok {
|
|
m[k] = t.Default.Nanoseconds()
|
|
}
|
|
}
|
|
}
|
|
|
|
// only add the Timeout to the Meta if we have values
|
|
if len(m) > 0 {
|
|
switch instance := ids.(type) {
|
|
case *tofu.InstanceDiff:
|
|
if instance.Meta == nil {
|
|
instance.Meta = make(map[string]interface{})
|
|
}
|
|
instance.Meta[TimeoutKey] = m
|
|
case *tofu.InstanceState:
|
|
if instance.Meta == nil {
|
|
instance.Meta = make(map[string]interface{})
|
|
}
|
|
instance.Meta[TimeoutKey] = m
|
|
default:
|
|
return fmt.Errorf("Error matching type for Diff Encode")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *ResourceTimeout) StateDecode(id *tofu.InstanceState) error {
|
|
return t.metaDecode(id)
|
|
}
|
|
func (t *ResourceTimeout) DiffDecode(is *tofu.InstanceDiff) error {
|
|
return t.metaDecode(is)
|
|
}
|
|
|
|
func (t *ResourceTimeout) metaDecode(ids interface{}) error {
|
|
var rawMeta interface{}
|
|
var ok bool
|
|
switch rawInstance := ids.(type) {
|
|
case *tofu.InstanceDiff:
|
|
rawMeta, ok = rawInstance.Meta[TimeoutKey]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
case *tofu.InstanceState:
|
|
rawMeta, ok = rawInstance.Meta[TimeoutKey]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
default:
|
|
return fmt.Errorf("Unknown or unsupported type in metaDecode: %#v", ids)
|
|
}
|
|
|
|
times := rawMeta.(map[string]interface{})
|
|
if len(times) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if v, ok := times[TimeoutCreate]; ok {
|
|
t.Create = DefaultTimeout(v)
|
|
}
|
|
if v, ok := times[TimeoutRead]; ok {
|
|
t.Read = DefaultTimeout(v)
|
|
}
|
|
if v, ok := times[TimeoutUpdate]; ok {
|
|
t.Update = DefaultTimeout(v)
|
|
}
|
|
if v, ok := times[TimeoutDelete]; ok {
|
|
t.Delete = DefaultTimeout(v)
|
|
}
|
|
if v, ok := times[TimeoutDefault]; ok {
|
|
t.Default = DefaultTimeout(v)
|
|
}
|
|
|
|
return nil
|
|
}
|