github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/resource_timeout.go (about)

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